Skip to content
Select theme

How to read a response header

Short answer: response.headers().get("header-name") hands back Option<&HeaderValue>. Header names are case-insensitive — reqwest stores them lowercased, so any spelling hits the same entry — and the value becomes text with .to_str(). When an API meters you, read x-ratelimit-remaining and x-ratelimit-reset and wait for the window rather than hammering the endpoint.

This picks up the fetch project from How to set a timeout on a request. The same two crates, no features beyond reqwest’s defaults:

Cargo.toml
[dependencies]
reqwest = "0.13.5"
tokio = { version = "1.53.3", features = ["macros", "rt-multi-thread"] }

Response::headers() gives you a &HeaderMap. A lookup is two small failures deep: get returns None when the server didn’t send the header, and a header value is bytes, so to_str() returns a Result that fails on values which aren’t plain ASCII. Both are worth handling where they happen:

cargo run prints (cargo’s own Compiling and Finished lines are not shown):

content-type: application/json
x-ratelimit-remaining: missing

The two crates re-export the map and its pieces — reqwest::header::{HeaderMap, HeaderName, HeaderValue} — so nothing from the http crate needs to be a direct dependency. to_str()’s error type is header::ToStrError, and it isn’t one of the errors reqwest::Error converts from, which is why the code above handles it locally instead of reaching for ?. A value that isn’t ASCII text still reads fine as bytes through as_bytes().

HTTP field names are case-insensitive on the wire: Content-Type, content-type and CONTENT-TYPE are the same field, and a server is free to send any of them. reqwest leans on that rather than fighting it. HeaderName lowercases on the way into the map, get lowercases whatever you pass it, and the map always reports names in their lowercased form:

X-RateLimit-Remaining -> Some(Ok("42"))
x-ratelimit-remaining -> Some(Ok("42"))
X-RATELIMIT-REMAINING -> Some(Ok("42"))
x-ratelimit-remaining
x-ratelimit-reset

Three spellings, one entry. That’s also why there’s no case-sensitive get and no way to store a header under a mixed-case name: by the time a name is in the map, its case is gone. The practical consequence is that your own code should stop writing the case the server happened to use — match on the lowercased form and it works against every server.

An API that meters you says so in its response headers. x-ratelimit-remaining counts the requests left in the current window and x-ratelimit-reset says when the window reopens, as epoch seconds. Reading them beats guessing a sleep, and it’s the difference between one well-timed wait and a retry loop that keeps you locked out. httpbin’s /response-headers endpoint echoes whatever headers you ask for, which makes the mechanics testable without an account:

out of requests; waiting for the window to reset
window reset

The reset value is a timestamp in seconds, so it only has one-second resolution — a wait computed from it can come out a second early, and a request that early can still be refused. The fix isn’t a cleverer formula: it’s checking the header again on the next response, which is what a loop around this does.

Two neighbouring conventions are worth knowing. Retry-After is the same idea sent from the other side, attached to a 429 or 503 response; it’s either a number of seconds or an HTTP date, and the date form needs a date parser. And header names aren’t standardised across APIs — GitHub spells them x-ratelimit-remaining, other services pick their own — so read each API’s docs rather than assuming a name.