What do OkHttp interceptors actually do?

Yes — interceptors are the mechanism OkHttp gives you to sit in the middle of every call your app makes. An interceptor can watch a request leave and a response come back, change either one on the way, and in some cases decide that the request should be tried again or never reach the network at all. OkHttp interceptors are the standard answer whenever a developer needs something to happen to all requests rather than to one call at a time.

Without interceptors, cross-cutting work gets copy-pasted everywhere: the same header added in twenty places, the same logging wrapped around every call, the same retry logic rewritten for each screen. Interceptors move that work into one registered component that the client runs automatically. Once you understand the chain they form, many other parts of OkHttp become easier to follow too.

This article explains what an interceptor does, how the two kinds differ, the order they run in, and the rules that keep them from breaking your calls. It also covers the mistakes that show up repeatedly when developers write their first ones.

A chain that every request and response passes through

The central idea is a chain. When your code executes a call, OkHttp does not send the request directly. Instead, it hands the request to the first interceptor, which may inspect or modify it and then passes it along to the next one. The last interceptor in the chain is the part that actually talks to the server, and the response travels back through the same interceptors in reverse.

Each interceptor receives a chain object. Calling proceed on that chain, with a request, moves the request one step closer to the network and eventually returns the response. Because an interceptor both hands work forward and receives the result, it can act twice: once on the way out and once on the way back.

That two-sided position is what makes interceptors so flexible. One component can stamp the time before proceeding, measure how long the call took when the response returns, and log both facts together. Nothing else in the library gives you that complete view of a single call.

Two kinds: application and network interceptors

Developers register interceptors in one of two lists on the client builder, and the position matters. Application interceptors run once per logical call, at the very outside of the chain. They see the request exactly as your code built it and the response exactly as it will be handed back, with redirects and retries already resolved into a single result.

Network interceptors sit further inside, close to the wire. They run once per actual network exchange, so a call that is redirected or retried passes through them several times. They see the raw request that goes onto the connection and the raw response that comes off it, including intermediate responses an application interceptor never observes.

A practical rule follows from this. If a concern belongs to your application — signing requests, adding an authorisation header, measuring user-visible timing — it belongs in an application interceptor. If a concern belongs to the connection itself, such as observing each redirect hop or the headers exactly as sent, a network interceptor is the right tool.

The order interceptors run in

Interceptors run in the order they were added to the builder. The first application interceptor added is the first to see the outgoing request and the last to see the returning response, because the chain wraps around everything registered after it. Thinking of them as nested layers, rather than a flat list, prevents most ordering surprises.

Order becomes important when interceptors depend on each other. A logging interceptor added before an authentication interceptor will print requests without the credential header, while one added afterwards will print the header too — a difference that matters for both debugging and privacy. When behaviour looks wrong, checking registration order is a good early step.

Developers’ own interceptors always wrap the built-in machinery. Application interceptors run first, then OkHttp’s internal interceptors, with network interceptors placed inside that internal sequence just before the connection is used. Your code always sees the call first.

Adding and changing headers in an interceptor

The most common job for an interceptor is attaching something to every request: an authorisation token, an API key, an accept header, or a user agent that identifies the app. The interceptor builds a modified copy of the incoming request with the extra header and proceeds with that copy, leaving the rest of the call untouched.

Because this happens centrally, rotating a token or changing a header value means editing one class instead of hunting through the codebase. It also guarantees consistency — no call site can forget the header, because no call site is responsible for it. Developers who later need per-request control over the same headers should read about how to add headers to OkHttp requests directly on the request builder.

Rewriting goes beyond headers. An interceptor can change the URL, for example to add a query parameter to every call or to route requests to a different host in a test environment. It can also modify the response on the way back, although responses are harder to change safely because their bodies are streams, as the safety rules below explain.

Logging requests and responses

Logging is the use case most developers meet first. A logging interceptor prints the request line, headers and timing, then prints the response status and, at its most verbose level, the response body. During development this makes network problems visible in seconds instead of requiring a proxy or guesswork.

The catch is the body. Reading a response body consumes it, so a logging interceptor that inspects the body must be careful to leave the stream usable for the code that actually needs the data. Well-built logging interceptors are written with that constraint in mind, but a hand-rolled one that reads the body carelessly can break the very calls it was meant to observe.

Logging also has a privacy dimension. Headers routinely carry tokens, and bodies routinely carry personal data. Verbose request logging belongs in debug builds, not in release builds where logs may be collected, shared in bug reports, or visible on a user’s device for far longer than intended.

Retrying and short-circuiting a call

Because an interceptor controls whether and how often proceed is called, it can retry. If the response shows a transient failure, or the first attempt throws an exception the interceptor knows is recoverable, it can close the failed response and call proceed again with the same or an adjusted request. Token refresh follows this pattern: send once, receive an unauthorised response, fetch a new token, then proceed again with it.

An interceptor can also refuse to proceed at all and return a response it created itself. That ability powers offline modes and testing setups, where a canned response is more useful than a network round trip. From the caller’s side, a short-circuited call looks exactly like a real one — which is why it is useful, and why it should be used deliberately.

Retries written this way need a firm limit. An interceptor that proceeds again on every failure without counting attempts can loop a broken request forever, hammering the server and draining the battery. A small maximum attempt count, and care about which failures are worth retrying, separates a helpful retry from a harmful one.

Measuring timing and observing traffic

Wrapping the proceed call with two timestamps gives an interceptor an accurate measure of how long the rest of the chain took. Aggregated over many calls, that data shows whether slowness comes from the network or from everything around it, and it can be attached to the request as a tag or reported to whatever monitoring the app already uses.

Network interceptors offer a finer-grained view. Because they run per exchange, they can observe each redirect, each retry of a failed connection, and the exact moment a response starts arriving. Investigations that separate connection setup from data transfer usually end up at this level.

Observation is also where interceptors help with debugging beyond logging. Counting failures by host, recording response codes, or flagging calls that exceed a time budget are all one small interceptor each. The pattern scales because each concern lives in its own class that can be added, removed, or reordered independently.

The interceptors OkHttp runs internally

Your interceptors are not the only ones in the chain. OkHttp itself is built from internal interceptors that divide the work of a call into stages. One stage handles retries and follows redirects, another bridges between application-level requests and wire format by adding content and cookie headers, and another checks the cache before any network traffic happens.

Later stages find or open a connection and then exchange the request and response with the server. You never register these yourself; they are inserted automatically inside the parts of the chain your interceptors wrap. Knowing they exist explains behaviour developers otherwise attribute to magic, such as a redirect being followed without their code seeing the intermediate response.

The practical takeaway is that the library’s headline features are not separate systems bolted on. Caching, redirect handling and connection reuse are stages of the same chain your code plugs into, which is why interceptors can observe and influence so much of what the client does.

Rules that keep an interceptor from breaking calls

A few rules keep interceptors safe. Call proceed exactly once for a normal pass-through; proceeding twice duplicates the request, and not proceeding at all is a deliberate choice that obliges you to return a response yourself. If you consume a response body inside an interceptor, you are responsible for what the next layer receives.

Exceptions deserve care as well. Let failures you do not understand propagate instead of swallowing them and returning a fabricated success, because callers make decisions based on those errors. Similarly, an interceptor that opens a response to peek at it must close it if it decides to proceed again, or the connection cannot be reused and the pool slowly starves.

A typical well-behaved interceptor is short enough to write once and reuse everywhere:

class TimingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val started = System.nanoTime()
        val response = chain.proceed(chain.request())
        val tookMs = (System.nanoTime() - started) / 1_000_000
        println("Request took $tookMs ms")
        return response
    }
}

Finally, keep interceptors focused. One interceptor that logs, authenticates, retries and rewrites URLs is four bugs waiting to be untangled. Several small interceptors, each doing one job, are easier to test, easier to order, and easier to remove when a requirement changes. The canonical starting point for all of them is the client itself, described on the OkHttp homepage.

Frequently Asked Questions

Can an interceptor change the response body before the app reads it?

Yes, but with care. The interceptor can read the body and return a new response carrying modified content, which is how some developers strip fields or reformat payloads centrally. Because bodies are one-shot streams, whatever the interceptor returns is what the caller gets — if it consumes the original and forgets to supply a replacement, the caller receives nothing useful.

Do interceptors run when a response comes from the cache?

Application interceptors always run, because they sit outside the cache stage of the chain. Network interceptors may not run for a cached response, since no network exchange takes place. This difference is a common reason a network-level logger appears to miss some calls.

Can two interceptors both modify the same request?

Yes. Each interceptor receives the request as modified by the ones before it, applies its own change, and proceeds. Requests are immutable, so every change produces a new copy passed down the chain. Conflicts are rare as long as each interceptor adjusts distinct parts, such as one adding a header and another adding a query parameter.

Should an interceptor handle every error itself?

No. An interceptor should only handle errors it genuinely understands, such as refreshing an expired token after an unauthorised response. Everything else should be thrown onwards so the caller can react. Converting unknown failures into fake successful responses hides outages and corrupts the state callers build.

Conclusion

OkHttp interceptors are a chain of checkpoints wrapped around every call: they observe requests and responses, rewrite them, retry them, and occasionally replace the network entirely. Application interceptors work at the level of logical calls, network interceptors at the level of individual exchanges, and OkHttp’s own features are built from the same mechanism inside.

Written small and kept honest — proceed once, respect the body, let unknown errors through — interceptors turn scattered networking work into a handful of reusable components. Most teams start with one for headers and one for logging, and that modest beginning usually covers the majority of what they ever need.

1 thought on “What do OkHttp interceptors actually do?”

  1. Pingback: How do you add headers to OkHttp requests? - OkHttp

Leave a Comment

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

Scroll to Top