How do you add headers to OkHttp requests?

Headers in OkHttp are added in two places, depending on how far they need to reach. For a single call, you set them on the request builder while the request is being created. For every call the client makes, you set them once inside an interceptor. Both approaches use the same pair of methods, header and addHeader, and understanding the difference between those two is most of what header handling involves.

The builder approach covers the majority of everyday work with OkHttp: an authorization token here, an Accept value there. The interceptor approach matters when a header must be impossible to forget, such as a token that every endpoint requires. This article covers both, along with the headers the client manages on its own and the mistakes that lead to duplicated or missing values.

Adding a Header to a Single Request

While you build a request, each header call returns the builder again, so headers chain naturally alongside the URL and the method. A typical request might set an Accept header to say what kind of response it wants and an Authorization header to prove who is asking. Once the request is built, its headers are fixed: a built request is immutable, and changing a header means building a new request from the old one.

That immutability surprises developers who try to adjust a shared request object between calls. The supported pattern is to keep a base builder or rebuild from the previous request with newBuilder, apply the change, and build again. It feels verbose, but it guarantees that the headers one call sends can never be mutated by another call happening at the same time.

header() Replaces, addHeader() Appends

The distinction between the two methods is small and crucial. Calling header with a name that already exists on the request discards the old value and installs the new one, so afterwards the request carries exactly one value for that name. Calling addHeader keeps whatever is already there and appends another value alongside it. Setting an authorization token is a replace job; collecting several accepted languages is an append job.

val request = Request.Builder()
    .url("https://example.org/profile")
    .header("Accept", "application/json")
    .addHeader("Accept-Language", "en")
    .addHeader("Accept-Language", "ur")
    .build()

Reaching for the wrong one produces bugs that look like server problems. If a token refresh sets the new token with addHeader instead of header, the request leaves carrying two Authorization values, the stale one and the fresh one, and many servers reject that or honour the wrong one. When behaviour seems inconsistent between calls, inspect the actual header values on the wire before suspecting the server.

When One Header Legitimately Has Several Values

HTTP has always allowed a header field to appear multiple times, and some headers make real use of it. A request can list several acceptable content types or languages, and a response can carry more than one cookie instruction or cache directive. OkHttp’s header model reflects this directly: headers are stored as an ordered list of name and value pairs, not as a map from names to single values.

Reading follows the same model. Asking for a single value by name returns the last one that was set, while asking for all values returns the complete list in order. When you debug a header problem, print the whole list rather than the single value. The missing or extra entry you are looking for is usually visible immediately once duplicates stop being hidden by a single-value lookup.

The Headers Developers Reach for Most

A handful of headers account for nearly all manual header work. Authorization carries credentials, most often a bearer token copied from wherever your app stores the session. Accept declares which response formats you can handle, and servers use it to choose between representations. User-Agent identifies your app and version, which many APIs rely on for diagnostics and rate-limit conversations.

Content-Type deserves a special mention because it is the one header developers most often set unnecessarily. When a request has a body, the content type belongs to the body itself and travels with it automatically. Setting it again on the builder creates two sources of truth. Decide header by header whether it describes the request, which is yours to set, or the payload, which the body should declare.

Adding a Header to Every Request at Once

Some headers belong to the client rather than to any individual call: an API key, an app version marker, or a token that must ride along everywhere. Repeating them at every call site invites the one forgotten request that fails in production. The central solution is an interceptor, a small piece of code that sees each outgoing request, rebuilds it with the extra header added, and passes it on.

Because this pattern sits at the heart of how the client can be extended, it is worth understanding what OkHttp interceptors do beyond headers as well. Inside the interceptor, prefer the replace style of header setting for credentials, so a request that already carries a value never ends up with two. One careful central implementation is safer than fifty careful call sites.

Headers the Client Sets on Its Own

Not every header on the wire was written by you. OkHttp adds several itself because they describe the connection rather than your intent. Host identifies the server being contacted. Content-Length states the exact size of the body, calculated by the client. Accept-Encoding advertises compression support, and when a response arrives compressed, the client decompresses it transparently and adjusts the related headers so your code sees the plain body.

Trying to override these managed values usually ends badly. A hand-set Content-Length that disagrees with the real body size will corrupt the request or hang the connection. The safe mental model is a division of labour: you own the headers that express your application’s meaning, such as authorization and preferences, while the client owns the headers that express the mechanics of this particular transfer.

Reading the Headers That Come Back

Responses carry headers too, and they are read through the same list-based model. A single-value lookup returns the last value for a name, which suits headers like Content-Type or a request identifier assigned by the server. The all-values lookup matters for responses that set several cookies or repeat a directive. Header values also arrive with their original names preserved, even though matching is forgiving about capitalization.

Response headers are diagnostic gold. The status code tells you the outcome in one number, but headers often explain it: a retry delay after rate limiting, a content type that reveals an error page instead of the expected data, or a server identifier that support teams ask for. When a call misbehaves, reading the response headers carefully is faster than guessing from the body alone.

Headers are also where servers negotiate the future of the conversation. A response may announce how long its content can be reused, which encoding it chose, or that the resource has moved and where it now lives. Your code does not need to act on most of these, since the client handles the mechanical ones itself, but knowing they exist turns a mysterious behaviour, such as a response arriving without a network trip, into an explainable one.

Removing a Header You No Longer Want

Builders also support removal. The removeHeader method strips every value stored under a name, which is the clean way to drop a header that a shared base request or an earlier interceptor added. Removal matters most in layered setups, where a general configuration adds a header by default and a specific endpoint must not send it, for example a public endpoint that should not receive credentials.

Removal inside an interceptor follows the same shape as addition: take the incoming request, produce a new one from its builder, remove or replace the header, and proceed with the rebuilt request. Because every change creates a new request object, the original stays untouched for anything else that holds it. Chains of these small rebuilds are how the client keeps concurrent calls isolated from one another.

Capitalization Does Not Matter

Header names are case-insensitive by specification, and OkHttp treats them that way when matching. Setting “authorization” and later asking for “Authorization” refers to the same field, and the replace behaviour of header() applies across casings. You do not need to normalize names across your codebase for correctness, although consistency helps humans read the code.

One caveat belongs to newer protocol versions. On the wire with HTTP/2, header names travel in lowercase by design, which is normal and not a sign that something rewrote your request. Servers are required to treat names case-insensitively, so a header that works over one protocol version works over the other. If a server behaves differently based on capitalization, the server is the problem.

Header Mistakes That Bite in Production

Two mistakes recur constantly. The first is duplicated credentials after redirects. When a request is redirected to a different host, the client deliberately strips the Authorization header so tokens do not leak to a server they were not meant for. Developers who re-add it blindly in a follow-up mechanism can recreate the leak the client was preventing. Let cross-host redirects drop credentials unless you fully control both hosts.

The second mistake is logging. During development it is natural to print complete requests, headers included, and those logs have a way of surviving into released apps and shared bug reports. Tokens, session cookies and API keys in a log file are credentials in the wild. Log header names freely, redact values for anything sensitive, and strip verbose request logging entirely from release builds.

Frequently Asked Questions

Why is my header missing when the request reaches the server?

Work through three suspects in order. The header may have been replaced by a later header() call with the same name, removed by an interceptor, or dropped deliberately, as Authorization is on cross-host redirects. Printing the final request’s full header list inside an interceptor shows exactly what left the device.

Can a request carry two Authorization headers?

Physically it can, because headers are a list, but servers are not obliged to accept it and many reject duplicates or read only the first. If your token logic can ever run twice for one call, one of the runs used the append method where it should have replaced. Use the replace form for credentials.

How do I change a header on a request I already built?

You cannot change the built request; you derive a new one. Call newBuilder on it, apply header changes there, and build again. This is deliberate, since built requests are shared freely between threads and calls, and mutating them would create exactly the race conditions the design avoids.

Should I set Content-Type myself on requests with a body?

Almost never. The body carries its own media type, and the client emits Content-Type from it. Setting the header separately risks two conflicting declarations when the body type changes later. The exception is a body type that genuinely cannot describe itself, which is rare in practice.

Conclusion

Adding headers in OkHttp is a small topic with sharp edges: set them per request on the builder or globally in an interceptor, replace with header() and append with addHeader(), and leave the mechanical headers to the client. Those few habits cover everything from a single Accept value to a token strategy for a whole app.

When a header behaves strangely, remember that the request you inspect in code is not always the request on the wire. Look at the complete header list late in the chain, after redirects, interceptors and replacements have all had their say, and the discrepancy will usually explain itself.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top