Free Networking Interview Questions & Answers

Retrofit, OkHttp, caching, auth, and resilient API calls.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What is Retrofit and what role does a converter factory play in a typical setup?

    Retrofit is a type-safe HTTP client that turns a Kotlin interface annotated with HTTP verbs into a working implementation, delegating the actual network I/O to OkHttp. It does not know how to serialize or deserialize bodies on its own, so you register a converter factory such as MoshiConverterFactory or KotlinSerializationConverterFactory when building the Retrofit instance. The order of converters matters because Retrofit asks each one in turn whether it can handle a given type, so a scalars converter for plain strings must be added before a JSON converter that would otherwise greedily try to parse everything. Without a matching converter for a return type you get an IllegalArgumentException at build or first-call time, not a runtime network error.

  2. When you declare a Retrofit method as a suspend function, what does Retrofit do under the hood and what thread does the body deserialization run on?

    Retrofit has a built-in suspend adapter, so since version 2.6 you no longer need a separate coroutine call adapter; a suspend function returning a body type is handled natively by bridging OkHttp's enqueue callback into a coroutine continuation. The actual request executes on OkHttp's dispatcher thread pool, and the response body is parsed by the converter on that background thread before the continuation resumes, so you do not block your calling coroutine. A common misconception is that you must wrap the call in withContext(Dispatchers.IO); that is unnecessary because Retrofit already moves the work off the caller, though wrapping does no harm. The suspend function resumes on whatever dispatcher your coroutine is confined to, not necessarily the main thread.

  3. What is the difference between returning a bare body type versus returning Response<T> from a Retrofit method?

    Returning a bare body type like a data class means Retrofit throws an HttpException for any non-2xx status and gives you no direct access to headers or the error body without catching that exception and digging into it. Returning Response wrapping your type hands you the full HTTP response so you can inspect isSuccessful, the status code, and headers, and read the error payload via errorBody yourself. The subtle trap is that errorBody can only be consumed once and is a streaming source, so calling errorBody().string() twice, or after the response is closed, throws. For robust error handling that maps server error JSON into a domain type, returning Response and manually branching on isSuccessful is usually the better choice.

  4. How do you distinguish a connectivity failure from a server error when calling Retrofit, and how would you map both into a sealed Result?

    A connectivity or transport problem, such as no network, DNS failure, or a timeout, surfaces as an IOException subclass, while a completed request that returned a non-2xx status surfaces as an HttpException carrying the code and error body. In a sealed Result you typically catch IOException and map it to a Network or Offline case, catch HttpException and map it to a Server case that carries the status code and parsed error body, and let anything else propagate or map to an Unknown case. The common mistake is catching a broad Exception and treating everything as offline, which hides real 4xx and 5xx server responses. Because HttpException means the socket round trip succeeded, retrying it blindly is often wrong, whereas retrying an IOException may make sense.

  5. What is the difference between an OkHttp application interceptor and a network interceptor, and which one sees a cache hit?

    An application interceptor sits at the top of the chain and is invoked exactly once per call, before OkHttp does any caching, redirect following, or retrying, so it does not see redirects or the intermediate requests OkHttp generates, and it may not run the actual network at all if the response is served entirely from cache. A network interceptor sits lower, between the cache and the socket, so it runs once per actual network request, meaning it fires multiple times across redirects and retries but is skipped entirely when a response is served from the cache. Therefore if you need to add an auth header to every logical call, use an application interceptor; if you need to observe or rewrite what actually goes over the wire, use a network interceptor. Confusing the two leads to bugs like logging that misses redirects or headers added to cached responses.

  6. Why should token attachment usually be an application interceptor rather than a network interceptor?

    You want the Authorization header applied to the logical request the caller intended, once, regardless of whether OkHttp ends up following redirects or retrying, and an application interceptor runs at exactly that level. A network interceptor runs per physical network request, so on a redirect it could leak the token to a different host you were redirected to, which is a security concern, and it would be skipped on cache hits, which is usually fine but not what you reason about for auth. Putting the token logic in an application interceptor keeps a single, predictable injection point. The one caveat is that application interceptors do not see automatically-followed redirects, so if a redirect target legitimately needs the token you must handle that explicitly rather than relying on the interceptor.

  7. Why is OkHttp's Authenticator the right tool for token refresh instead of a plain interceptor, and what does it give you that an interceptor does not?

    OkHttp's Authenticator is invoked reactively only after the server actually returns a 401 (or 407 for proxies), and its return value is used to build a retried request automatically, which matches the real semantics of an expired token far better than proactively guessing in an interceptor. It also gives you the prior Response so you can read the challenge and count how many times you have already tried, letting you give up after a bounded number of attempts to avoid an infinite loop. A plain interceptor would have to inspect every response for 401 and manually reissue the request, duplicating logic OkHttp already provides. The critical gotcha is that Authenticator can be called from multiple threads at once, so refresh must be synchronized.

  8. How do you handle many concurrent requests all getting 401 at once so you refresh the token only once (single-flight refresh)?

    The naive approach refreshes the token independently inside each Authenticator invocation, so ten in-flight requests trigger ten refresh calls, and the later ones may even invalidate the token the earlier ones just obtained. The correct pattern is a single-flight refresh: guard the refresh with a lock or mutex, and inside the critical section first check whether the token has already been updated since this request's failed attempt, by comparing the token the failed request carried against the current stored token. If it already changed, just retry with the new token instead of refreshing again; only actually hit the refresh endpoint if you still hold the stale token. This collapses the stampede into one network refresh while every waiter proceeds with the fresh credential.

  9. How does OkHttp's Authenticator let you stop an infinite refresh loop when the refresh itself keeps yielding an unauthorized result?

    Authenticator returning a non-null request tells OkHttp to retry, and if that retry also gets a 401 the Authenticator is called again, so a server that always rejects would loop forever unless you cap it. You bound it by counting the number of prior responses via the responseCount helper that walks response.priorResponse, and returning null once you have retried some small number of times, which makes OkHttp give up and surface the 401 to the caller. Returning null is the explicit signal that you cannot satisfy the challenge. Forgetting this bound is a classic production bug where an invalidated refresh token pins a thread in a tight retry loop hammering the auth server.

  10. What does OkHttp's connection pool do, and why does reusing an OkHttpClient instance matter?

    OkHttp maintains a connection pool that keeps idle keep-alive HTTP/1.1 and HTTP/2 connections open for reuse, so subsequent requests to the same host can skip the TCP and TLS handshake, which is the expensive part of a request. This pooling, along with the shared dispatcher thread pool and cache, is tied to a single OkHttpClient instance, so creating a new client per request throws away pooled connections and defeats the optimization while leaking threads and sockets. The correct guidance is to create one OkHttpClient for the app and derive variants with newBuilder so they share the underlying pool, dispatcher, and cache. HTTP/2 additionally multiplexes many streams over one connection, so a single pooled connection can serve concurrent requests to the same host.

  11. What do Cache-Control, ETag, and a conditional GET have to do with OkHttp's HTTP cache, and what must you configure for caching to work at all?

    OkHttp implements a standards-compliant HTTP cache but only if you assign a Cache object to the client, since there is no on-disk cache by default. Given a cache, freshness is driven by response headers: Cache-Control with max-age lets OkHttp serve a stored response without any network, while an ETag or Last-Modified lets OkHttp send a conditional GET with If-None-Match, and if the server replies 304 Not Modified, OkHttp returns the cached body without re-downloading it. The common misunderstanding is thinking you enable caching in code with a flag; instead it is the server's headers plus an installed Cache that govern behavior. If the server sends no cache-related headers, nothing is stored.

  12. Why is a POST request not cached by OkHttp's HTTP cache, and how does that differ from GET?

    The HTTP caching model that OkHttp follows caches responses to safe, cacheable methods, and GET is the canonical cacheable method, whereas POST is defined as non-idempotent and state-changing, so its responses are generally not stored or reused. This is a protocol-level rule, not an OkHttp quirk, and it is why read APIs are modeled as GET when you want caching benefits. If you have data that is genuinely read-only but must be sent as POST because of a large query body, you will not get transparent HTTP caching for it and must handle caching yourself at the application layer. Trying to force caching of POST by fiddling with headers does not portably work.

  13. How do you serve responses from the OkHttp cache while offline, and what is the pitfall with the required header?

    You can force cache-only behavior by attaching a CacheControl with onlyIfCached to the request, which tells OkHttp to return a stored response or a 504 if none is fresh, and you typically add an interceptor that, when the device is offline, rewrites the request to allow a stale response via max-stale. The pitfall is that a response is only reusable offline if it was cacheable in the first place, meaning the server sent appropriate freshness headers, so if the origin marked responses no-store you have nothing to serve. A second pitfall is that onlyIfCached with a hard max-age requirement yields a 504 that you must handle, then fall back to a normal network request when connectivity returns. Simply requesting the cache does not conjure data that was never stored.

  14. What is certificate pinning in OkHttp and what is the operational risk that makes it dangerous if done carelessly?

    Certificate pinning uses a CertificatePinner to require that the server's certificate chain contains a certificate whose public key hash matches one you hardcoded, which defends against a compromised or rogue certificate authority issuing a fraudulent certificate for your domain. The operational risk is rotation: certificates and keys expire and get replaced, and if you pinned only the leaf key and the server rotates it, every installed app that cannot update instantly is hard-bricked from the API until users update. The mitigation is to pin a backup key or a higher intermediate that you control, include multiple pins, and ship pin updates well ahead of rotation. Network security config on Android can also express pins declaratively, but it carries the same rotation hazard.

  15. What is Android's network security config and what is a common trap when trying to use a debugging proxy?

    Network security config is an XML resource referenced from the manifest that declaratively controls TLS behavior, such as which certificate authorities are trusted, whether cleartext HTTP is allowed per domain, and certificate pins. A frequent trap developers hit is that since Android 9, cleartext traffic is disabled by default, so a plain http endpoint or a local dev server fails until you add a domain-specific cleartext permission. Another trap is that apps targeting modern Android do not trust user-added CA certificates by default, so a proxy like Charles or mitmproxy that installs a user certificate is silently ignored unless a debug-only network security config explicitly trusts the user trust anchor. Keeping that trust in a debug-only overlay avoids weakening production security.

  16. How does OkHttp handle gzip transparently, and when does that transparency break?

    If you do not set an Accept-Encoding header yourself, OkHttp automatically adds Accept-Encoding gzip, and when the server responds with a gzipped body it transparently decompresses it and strips the Content-Encoding and Content-Length headers so your code sees plain bytes. The transparency breaks the moment you set your own Accept-Encoding header, because OkHttp then assumes you want to handle encoding yourself and will hand you the raw, still-compressed bytes without decompressing. This bites people who add Accept-Encoding to force gzip and then wonder why their JSON parser sees garbage. The rule is to let OkHttp manage gzip automatically unless you have a specific reason to take over, in which case you must decompress manually.

  17. What is the difference between connect, read, write, and call timeouts in OkHttp?

    Connect timeout bounds how long establishing the TCP and TLS connection may take, read timeout bounds the maximum idle gap while waiting for the next byte of the response, and write timeout bounds the maximum idle gap while sending the request body, all of which are per-socket-operation limits rather than limits on the whole call. Call timeout, by contrast, bounds the entire duration of the call end to end, including DNS, connection, redirects, and reading the full body. The subtle trap is that read timeout resets on each received chunk, so a slow server dribbling one byte at a time can keep a request alive far longer than the read timeout suggests, which is exactly why callTimeout exists as an absolute ceiling. For large downloads you often set a generous callTimeout of zero to disable it and rely on the granular ones.

  18. Which HTTP requests are safe to retry automatically, and why does idempotency matter for retries?

    A retry is safe only when repeating the request cannot cause additional side effects, which is the definition of idempotency, so GET, PUT, and DELETE are generally idempotent while POST typically is not because it may create a new resource each time. Retrying a non-idempotent POST after a timeout is dangerous because you cannot tell whether the server processed the first attempt, so a naive retry can create duplicate orders or double charges. The correct approach for operations that must be retryable is to make them idempotent explicitly, for example by having the client send an idempotency key that the server uses to deduplicate. OkHttp's own automatic retry on connection failure is conservative and does not retry a request whose body was already partially transmitted in an unsafe way.

  19. Why is exponential backoff with jitter preferred over fixed-interval retries?

    Fixed-interval retries from many clients tend to synchronize, so after a server hiccup every client retries at the same instant, producing a thundering herd that keeps the recovering server overloaded, a self-inflicted retry storm. Exponential backoff spreads attempts out by increasing the delay after each failure, which reduces pressure, but pure exponential backoff still lets clients that failed together stay in lockstep. Adding jitter, a randomized component to each delay, decorrelates the clients so their retries scatter across time and the load smooths out. You also cap the maximum delay and the number of attempts, and you should only apply this to retryable, idempotent failures rather than to genuine 4xx client errors that will never succeed on retry.

  20. What is the practical difference between Moshi, Gson, and kotlinx.serialization for parsing JSON on Android?

    Gson uses runtime reflection and predates Kotlin, so it does not understand Kotlin nullability or default values and can construct objects with null in non-null fields via unsafe allocation, which leads to crashes later. Moshi is Kotlin-aware and, with its codegen annotation processor, generates adapters at compile time that respect non-null types and default values and avoid reflection at runtime. kotlinx.serialization is the Kotlin-first library that generates serializers at compile time from the Serializable annotation, integrates cleanly with the language, and is the common modern default especially in multiplatform code. The key point is that Gson's Kotlin-unawareness is a correctness hazard, whereas Moshi codegen and kotlinx.serialization enforce the type contract at the boundary.

  21. Why can a JSON field declared non-null in a Kotlin data class still crash your app at parse time, and how do the parsers differ?

    If the server omits a field or sends explicit null for it, a truly type-safe parser must reject the value because your Kotlin type promised it is non-null, and the crash you get is the parser correctly refusing to violate the contract. Moshi codegen and kotlinx.serialization detect the missing or null value and throw a clear exception at the boundary, which is preferable to letting a null leak deep into your code. Gson via reflection instead may silently leave the field null despite the non-null declaration, so the crash happens much later at an unrelated call site, making it far harder to diagnose. The fix is to model optional fields as nullable types or provide default values so the schema matches reality.

  22. How do Moshi and kotlinx.serialization handle unknown keys and default values differently, and why does it matter for API evolution?

    By default both libraries ignore unknown JSON keys, which is what you want so that a server adding a new field does not break older clients, but kotlinx.serialization is strict about unknown keys unless you configure ignoreUnknownKeys true on the Json instance, so forgetting that flag causes it to throw when the API adds fields. Default values let a field absent from the payload fall back gracefully, but there is a catch: kotlinx.serialization by default does not require present-but-default values to be encoded and, importantly, uses the default only when the key is absent, not when it is explicitly null. Getting these settings wrong means either brittle clients that crash on API additions or silent data loss. Matching the parser configuration to how your backend evolves is the real skill.

  23. How do you upload a file with Retrofit as multipart without loading the whole file into memory?

    You model the endpoint with the Multipart annotation and Part parameters, and instead of reading the file into a byte array you wrap it in a RequestBody that streams from the source, for example by creating a RequestBody backed by the file or by overriding writeTo to copy from a BufferedSource. The naive approach of reading the entire file into a ByteArray and calling toRequestBody works for small files but causes OutOfMemoryError on large uploads. Using a streaming RequestBody, ideally one whose contentLength is known so the server gets a Content-Length rather than chunked transfer, keeps memory bounded. On modern Android you also resolve content URIs through the ContentResolver and stream from the InputStream rather than assuming a filesystem path.

  24. How do you download a large file with Retrofit and OkHttp without running out of memory?

    You declare the endpoint to return a streaming ResponseBody and annotate the method with Streaming, which tells Retrofit not to buffer the entire body into memory before handing it to you. Then you read from the response's byteStream or source in a loop and write to disk incrementally, closing the body when done. Without the Streaming annotation Retrofit materializes the whole response in memory, which defeats the purpose and can OOM on large files. You should also be mindful of timeouts here, since a multi-minute download will trip a default callTimeout, so you often disable callTimeout for these calls while keeping read timeout as a stall detector. Always close the ResponseBody to release the connection back to the pool.

  25. In Paging 3, what is a RemoteMediator and how does it coordinate the network with the local database?

    RemoteMediator is the Paging 3 component for a network-plus-database setup where the local database is the single source of truth and the network is used to fill it, as opposed to a PagingSource that pages directly from one source. Its load function is called with a LoadType of Refresh, Prepend, or Append when the database runs low on data in a direction, and you fetch the corresponding page from the network and insert it into the database within a transaction, often maintaining a separate remote keys table to track the next and previous cursors. The UI observes the database through a PagingSource, so inserts trigger updates automatically. The common mistake is trying to store paging cursors in memory rather than in the database, which breaks after process death, and forgetting to clear keys on a refresh, which corrupts pagination.

  26. What is the difference between offset-based and cursor-based pagination and why does cursor-based avoid a subtle bug?

    Offset pagination asks for items using a page number or a numeric offset and a limit, which is simple but breaks under concurrent inserts and deletes because the underlying list shifts between requests, causing you to skip or duplicate items as the offsets no longer line up. Cursor pagination instead returns an opaque cursor pointing to a stable position, typically encoding the sort key of the last item, so the next request says give me items after this cursor, which remains correct even if rows were added or removed. The tradeoff is that cursors do not let you jump to an arbitrary page and are harder to implement server side. For infinite-scroll feeds that mutate frequently, cursor pagination is the correct default, and pairing it with Paging 3 RemoteMediator keys is idiomatic.

  27. What are the tradeoffs between a WebSocket and Server-Sent Events for a live feature, and how do reconnections differ?

    A WebSocket is a full-duplex channel over a single TCP connection so both client and server can push messages at any time, which suits chat and collaborative editing, whereas Server-Sent Events is a one-way stream from server to client over a long-lived HTTP response, which suits notifications and live scores where the client rarely needs to push. SSE is simpler, rides ordinary HTTP so it plays nicely with proxies and standard auth, and has built-in automatic reconnection with a last-event-id so the server can resume from where the client left off. WebSockets have no built-in resume semantics, so you must implement reconnection, backoff, and message replay yourself. OkHttp supports WebSockets natively via newWebSocket, and you must handle onFailure by reconnecting with exponential backoff rather than assuming the socket stays open.

  28. What is the difference between the Ktor client engine and Ktor client plugins, and why does the engine choice matter on Android?

    In the Ktor client the engine is the pluggable transport that actually performs requests, such as OkHttp, CIO, or Android, while plugins, formerly called features, are cross-cutting behaviors installed onto the client like ContentNegotiation for JSON, Logging, Auth, and HttpTimeout. The engine choice matters because on Android the OkHttp engine gives you the mature connection pooling, HTTP/2, and interceptor ecosystem you may already depend on, whereas CIO is a pure-Kotlin coroutine engine with no OkHttp dependency that is good for multiplatform but has different capabilities. Plugins are engine-agnostic in principle, but some behaviors like certificate pinning are configured through the engine's underlying client. Choosing OkHttp as the Ktor engine lets you reuse existing interceptors while keeping Ktor's ergonomic API.

  29. How would you implement request deduplication or in-flight coalescing so identical concurrent calls share one network request?

    The idea is that when several callers ask for the same resource at nearly the same time, you should perform one network request and give all of them the same result rather than firing duplicates. You keep a map keyed by the request identity from the resource key to an in-flight Deferred or shared Flow, and when a call arrives you either start a new Deferred and store it or, if one already exists for that key, await the existing one. Once the request completes you remove the entry so future calls trigger a fresh fetch. The tricky parts are making the map access thread-safe with a mutex, ensuring the entry is removed even on failure so a failed request does not poison the key forever, and choosing a key that correctly captures request identity including relevant parameters.

  30. Why is HttpLoggingInterceptor with BODY level a privacy and security risk, and how should you configure logging safely?

    At BODY level HttpLoggingInterceptor logs full request and response bodies plus all headers, which means it will happily write Authorization tokens, cookies, passwords, and personally identifiable information into logcat, where other apps or crash reporters may capture it, so shipping BODY logging in a release build is a real data leak. The correct configuration is to install it only in debug builds, and even then to use redaction via redactHeader for sensitive headers like Authorization and Cookie so tokens do not appear. In production you either omit the interceptor entirely or drop to NONE or a minimal level. It should also be added as the last application interceptor or as a network interceptor depending on whether you want to log before or after other interceptors modify the request.

  31. On what thread does Retrofit invoke enqueue callbacks by default, and how does that differ on Android versus a plain JVM?

    Retrofit's asynchronous enqueue delivers its onResponse and onFailure callbacks on a thread chosen by the configured callback executor, and on Android Retrofit installs a default callback executor that posts results to the main thread, so your callback runs on the UI thread ready to touch views. On a plain JVM without that platform default, callbacks run on OkHttp's background dispatcher thread instead. This platform-dependent behavior surprises people who assume it is always the main thread or always a background thread. With suspend functions the picture is different again, since there is no callback executor involved and the coroutine simply resumes on its own dispatcher, which is why modern code favors suspend to avoid reasoning about the callback executor at all.

  32. What is the difference between codegen and reflection-based adapters in Moshi, and why prefer codegen on Android?

    Moshi can build adapters either at runtime through the KotlinJsonAdapterFactory, which uses Kotlin reflection to read constructor parameters and their nullability, or at compile time through the moshi-kotlin-codegen annotation processor triggered by the JsonClass annotation with generateAdapter true. Reflection pulls in the sizable kotlin-reflect library, is slower on first use, and does more work at runtime, whereas codegen generates a concrete adapter per class with no reflection, smaller runtime cost, and errors surfaced at compile time. On Android, where startup time, method count, and app size matter, codegen is the recommended path. A subtle trap is mixing them or forgetting the JsonClass annotation, which silently falls back to reflection only if you registered the reflection factory, otherwise failing.

  33. What actually happens on the wire in a conditional GET returning 304, and how does OkHttp merge the cached and new responses?

    When a cached response has an ETag or Last-Modified but is stale, OkHttp issues a network request including If-None-Match or If-Modified-Since, and if the resource is unchanged the server returns 304 Not Modified with no body, which saves downloading the payload. OkHttp then serves the cached body but updates the stored response's headers with any newer ones from the 304, and it exposes this to you: the response's networkResponse reflects the 304 while cacheResponse reflects the stored entry, and the returned response body is the cached one. The subtlety is that the header merge can change caching directives, and that a 304 still costs a full network round trip, so it saves bandwidth but not latency. If the server ignores conditional headers you get a full 200 instead.

  34. How should you securely store an authentication token on Android in 2024 and 2025, and what is wrong with common shortcuts?

    The common shortcut of stashing a token in plain SharedPreferences is insecure because on a rooted or compromised device the file is readable, and even the once-recommended EncryptedSharedPreferences from Jetpack Security is now deprecated. The current guidance leans on hardware-backed keys via the Android Keystore, where the key material never leaves the secure hardware, and you encrypt the token with a Keystore key before persisting the ciphertext, optionally gating key use behind biometric or device credential authentication. For short-lived access tokens, keeping them only in memory and relying on a securely stored long-lived refresh token reduces exposure. The deeper point is that no client storage is fully safe against a rooted device, so you minimize token lifetime and scope and never hardcode secrets in the APK.

  35. How do you approach API versioning from the client, and what breaks if you ignore backward compatibility in your models?

    Server-side versioning is usually expressed either in the URL path like a v1 or v2 segment or in an Accept header requesting a specific media type version, and the client pins to a version so a server rollout does not silently change response shapes underneath it. On the client side the real discipline is tolerant reading: model responses so that new unknown fields are ignored, optional fields are nullable or defaulted, and enums have a catch-all unknown case, so a server that adds a field or a new enum value does not crash older installs. The failure mode people hit is a strict parser that throws on an unrecognized enum value, which bricks the feature for everyone running the old app when the backend ships a new value. Designing models to degrade gracefully is what makes versioning survivable.

  36. What are the basics of how OkHttp handles DNS and IPv6, and what is the failover behavior you should know about?

    OkHttp resolves a hostname through a pluggable Dns interface that by default uses the system resolver, and a hostname can resolve to multiple addresses including both IPv6 and IPv4. OkHttp attempts these addresses and, following Happy Eyeballs style behavior, will fall back to another address, such as trying IPv4 after IPv6, if the first attempt fails to connect, which improves reliability on networks where one family is broken. This matters because a naive assumption that a host has a single IP leads to confusion when connections succeed intermittently. You can supply a custom Dns implementation for scenarios like DNS-over-HTTPS or pinning resolution, and OkHttp ships an optional DnsOverHttps helper. The key insight is that address selection and failover are handled for you but depend on correct multi-address resolution.

  37. Why can adding a manual Accept-Encoding or Content-Length header cause subtle bugs with OkHttp?

    OkHttp manages several headers automatically, and overriding them shifts responsibility to you in ways that are easy to get wrong. Setting Accept-Encoding disables transparent gzip decompression so you receive compressed bytes you must inflate yourself. Setting Content-Length manually can conflict with the RequestBody's own reported length, and if they disagree the request is malformed. Similarly OkHttp adds Host, Connection, and User-Agent if you do not, so duplicating them can produce unexpected values. The general rule is to let OkHttp own transport-level headers unless you have a concrete reason and fully understand the consequence, because these bugs manifest as corrupted bodies or servers rejecting the request rather than as obvious errors at the point where you set the header.

  38. How does HTTP/2 multiplexing change how you think about concurrency and connection limits compared to HTTP/1.1?

    Under HTTP/1.1 each connection carries one request at a time, so OkHttp opens several parallel connections to a host up to a per-host limit to achieve concurrency, and head-of-line blocking at the connection level is real. HTTP/2 multiplexes many concurrent streams over a single connection, so OkHttp typically uses just one connection per host even under heavy concurrency, which reduces handshakes and resource use. This means the old instinct to shard requests across many connections is counterproductive on HTTP/2, and the dispatcher's max-requests-per-host tuning behaves differently. A remaining subtlety is that HTTP/2 still suffers TCP-level head-of-line blocking under packet loss, which is what HTTP/3 over QUIC addresses, though HTTP/3 support on OkHttp is not the default.

  39. How do you read and parse a server error body in Retrofit when the call returns a non-2xx status, and what is the resource-management trap?

    When you return Response and the call is not successful, the parsed body is null and the error payload lives in errorBody, which is a raw ResponseBody you must deserialize yourself, typically by obtaining a converter from your Retrofit instance via responseBodyConverter for your error type and passing errorBody to it. The resource trap is that errorBody is a one-shot stream backed by the network source, so reading it consumes it, and calling string on it buffers the whole thing into memory then closes it, meaning you cannot read it twice. If you peek at it for logging and then try to parse it again you get an empty or closed-stream error. The clean approach reads it exactly once into your error model and, if you need the raw text too, use peekBody to get a bounded copy.

  40. What is the difference between OkHttp's automatic retry-on-connection-failure and application-level retries, and why is relying only on the former insufficient?

    OkHttp has a retryOnConnectionFailure setting, on by default, that transparently retries when a pooled connection turns out to be stale or a route fails, choosing another route or a fresh connection, but this only covers connection-level problems and never retries based on the HTTP status code. So a 500 or 503 from the server, or a timeout after the request reached the server, is surfaced to your code without any retry, because OkHttp cannot know those are safe to repeat. Application-level retry logic is where you decide, per endpoint and per idempotency guarantee, whether to retry a 503 with backoff or to give up on a 4xx. Confusing the two leads people to think OkHttp already handles all retries when in fact it deliberately stays out of application semantics.

  41. Why might a certificate pin configured in OkHttp fail even though the certificate is valid, and how does pin matching actually work?

    CertificatePinner matches the SHA-256 hash of the Subject Public Key Info of a certificate in the presented chain, not the whole certificate, and it succeeds if any pinned hash matches any certificate in the chain, so a valid TLS certificate can still fail pinning if none of its public keys correspond to your pins. This commonly happens when the server rotates to a new key, switches certificate authorities, or when you pinned the leaf but the operator changed it, or conversely when you computed the pin from the wrong certificate in the chain. Because pinning is orthogonal to normal chain validation, the TLS handshake trusts the certificate yet OkHttp throws SSLPeerUnverifiedException listing the actual pins seen, which is your clue. The fix is pinning a stable intermediate or including backup pins for the next key.

  42. How does the Retrofit CallAdapter mechanism relate to suspend functions and to returning Call, Flow, or RxJava types?

    A CallAdapter tells Retrofit how to translate an OkHttp Call into the return type your interface method declares, so returning a bare Call needs no adapter, returning an RxJava Single needs the RxJava call adapter factory, and returning a Flow-like type needs an appropriate adapter. Suspend functions are special because Retrofit handles them with built-in support rather than an external call adapter, internally adapting the Call to a coroutine continuation. A common confusion is thinking you must add a coroutines call adapter dependency for suspend functions, when in fact none exists as a separate artifact because it is built in. If you want Retrofit to return a cold Flow you generally still model the method as suspend and wrap the call yourself, or use a library adapter, since Retrofit does not natively page a Flow of a single response.

  43. What does it mean that OkHttp interceptors form a chain, and why does the order in which you add them change behavior?

    Each interceptor receives a Chain and is responsible for calling chain.proceed to pass control down to the next interceptor, so they nest like layers of an onion where code before proceed runs on the way down and code after proceed runs on the way up with the response. Because of this the order you add application interceptors determines who wraps whom: an interceptor added earlier wraps those added later, so a logging interceptor added first sees the request before a later auth interceptor adds the token, meaning it would log a request without the Authorization header. Reversing them logs the fully-decorated request. This ordering effect is why logging is often added last among application interceptors, and why understanding the onion model is essential to placing retry, auth, and logging correctly.

  44. How do you correctly cancel an in-flight Retrofit call when using coroutines, and what is the pitfall with structured concurrency?

    With suspend Retrofit functions, cancelling the surrounding coroutine, for example when a ViewModel scope is cleared, propagates cancellation into the awaiting continuation and Retrofit cancels the underlying OkHttp call, aborting the socket work, which is exactly the behavior structured concurrency gives you for free. The pitfall is running the call in a scope that is not tied to the screen's lifecycle, such as a GlobalScope or a scope that outlives the consumer, in which case cancellation never arrives and the request completes wastefully or updates a dead view. Another subtlety is that cancellation throws CancellationException, which you must not swallow in a broad catch that maps everything to an error state, otherwise you convert a normal cancellation into a spurious user-facing error. Rethrowing CancellationException while handling other exceptions is the correct pattern.

  45. Why can transparent gzip make a response's reported Content-Length misleading, and how should you handle progress for downloads?

    When OkHttp transparently decompresses a gzip response it strips the Content-Length header because the original length described the compressed bytes, not the decompressed body it hands you, so after automatic decompression there is no reliable total length to compute a percentage against. If you build a download progress bar by reading Content-Length, it may be absent or, if you disabled transparent gzip, may reflect compressed size while you count decompressed bytes, giving wrong percentages. The robust approach for progress is to add a network interceptor that wraps the ResponseBody and counts bytes as they are read from the still-encoded source, or to ensure the server sends an accurate length for non-compressed downloads. Assuming Content-Length is always present and always matches the bytes you read is the mistake.

  46. What is the danger of using a single shared Moshi or Retrofit instance incorrectly versus creating many, and where is the real cost?

    The expensive objects to build are the OkHttpClient, because it owns the connection pool, dispatcher threads, and cache, and to a lesser degree the converter, because building adapters involves reflection or codegen lookup, so these should be created once and shared, and Retrofit and Moshi instances are cheap to reuse and thread-safe. The mistake in one direction is constructing a fresh OkHttpClient per request, which discards pooled connections and leaks resources; the mistake in the other direction is sharing a client whose interceptors capture per-request or per-user state, such as baking a specific user's token into an interceptor closure, which then leaks across users. The correct model shares the heavy client and reads mutable auth state from a provider inside the interceptor rather than capturing it at construction time.

  47. How do you handle a server that returns 200 with an error described in the JSON body rather than using HTTP status codes, and why is this tricky with Retrofit?

    Some APIs always return 200 and encode success or failure inside the payload, which defeats Retrofit's normal assumption that non-2xx means failure, so isSuccessful is true and no HttpException is thrown even though the operation failed logically. This means your standard error mapping that keys off HTTP status never fires, and the failure silently flows into your success path. The correct handling is to model the response envelope explicitly with a status or error field, inspect it after deserialization, and convert a logical error into your sealed Result error case manually. The trap is that these responses may also use a union shape where success and error have incompatible bodies, which a single data class cannot parse, forcing you to deserialize into a wrapper first and branch, sometimes with a custom adapter, before reading the payload.

  48. What are the correctness concerns when reusing an OkHttp Call, and why must you clone it to execute twice?

    An OkHttp Call object represents a single request-response exchange and is one-shot, so once you have called execute or enqueue on it you cannot call it again, and attempting to do so throws an IllegalStateException about the call being already executed. If you need to run the same request twice, for instance in a retry that you implement manually outside interceptors, you must obtain a fresh Call via clone, which produces a new Call with the same request ready to execute. This bites people who cache a Call reference and try to reuse it, or who build retry logic at the wrong layer. Retrofit hides this by creating a fresh Call per method invocation, but if you drop down to raw OkHttp for streaming or custom flows you must respect the one-shot contract and clone deliberately.

  49. How do you build a robust WebSocket reconnection strategy in OkHttp, and what state consistency problems must you guard against?

    OkHttp delivers WebSocket lifecycle events to a WebSocketListener, and a robust client treats onClosed and especially onFailure as triggers to reconnect using exponential backoff with jitter and a cap, while resetting the backoff after a successful stable connection so transient drops do not permanently inflate delays. The hard part is state consistency: while disconnected you may buffer outgoing messages, but on reconnect you must reconcile with the server, since messages sent during the gap may be lost and the server may have advanced state, so you often resubscribe and request a resync using a last-seen sequence or cursor rather than assuming continuity. You must also guard against multiple overlapping reconnect attempts by tracking connection state, and avoid reconnecting when the app is backgrounded or the user logged out. Naive immediate reconnect loops both hammer the server and duplicate connections.

  50. How would you architect a networking layer that coalesces in-flight requests, respects a single-flight token refresh, caches offline, and maps errors, without those concerns interfering with each other?

    You separate the concerns into distinct layers so each has one responsibility: OkHttp interceptors handle transport concerns, with an Authenticator owning single-flight token refresh guarded by a mutex that re-checks the current token before refreshing, a cache-control interceptor rewriting requests to allow stale responses when offline, and a logging interceptor confined to debug builds with redacted secrets. Above OkHttp, Retrofit with a converter deserializes bodies, and a thin repository layer wraps calls to map HttpException and IOException into a sealed Result while performing in-flight coalescing through a keyed map of Deferred results guarded by its own mutex. The reason this composes cleanly is that refresh is reactive and lives in the Authenticator so it never races with the coalescing map, caching is expressed through standard headers so it is transparent to the repository, and error mapping happens once at the boundary where HTTP semantics are translated into domain outcomes. Keeping these orthogonal, rather than cramming refresh and dedup and caching into one interceptor, is what prevents deadlocks and subtle interference.

Practice all Networking questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics