Geonode logo
Geonode Team

Geonode Team

Updated: September 2, 2026

Published: 2026-09-02

OkHttp in Java: Getting Started Guide

OkHttp is the HTTP client most JVM and Android projects end up using, and it has two design decisions that catch newcomers: the client is meant to be shared, and response bodies must be closed. Get those right and everything else is straightforward. Get them wrong and you leak connections until something falls over. This guide covers the basics, interceptors, timeouts and proxies — plus a note on where the project now lives, which has changed.

Our stake: we are Geonode and we sell proxies, and OkHttp's proxy configuration is genuinely unusual — proxy authentication goes through a proxyAuthenticator, not a header, and getting that wrong produces a 407 that looks like a credentials problem when it is a configuration problem. That section is near the end. Everything before it works without any proxy at all, and if you are learning the library, learn it against your own connection first.

Where OkHttp Lives Now

Worth stating up front, because the old links are dead.

OkHttp's repository has moved. github.com/square/okhttp now redirects to github.com/lysine-dev/okhttp, and the documentation site that lived at square.github.io/okhttp returns a 404 — the current site is at lysine.dev/okhttp. The project is actively maintained and Apache-2.0 licensed, as verified in September 2026.

The Maven coordinates have not changed. It is still published as com.squareup.okhttp3:okhttp:

implementation("com.squareup.okhttp3:okhttp:5.5.0")

There is also a bill of materials to keep related artefacts in step:

implementation(platform("com.squareup.okhttp3:okhttp-bom:5.5.0"))

Requirements: Android 5.0+ (API level 21+) and Java 8+. OkHttp depends on Okio for I/O and on the Kotlin standard library — both described by the project as "small libraries with strong backward-compatibility". On Android it uses AndroidX Startup, and if you disable the initialiser in the manifest, your app must call OkHttp.initialize(applicationContext) in Application.onCreate.

The old 3.12.x branch supports Android 2.3+ and Java 7, and the project is blunt that those platforms "lack support for TLS 1.2 and should not be used".

Your First Request

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

Three things in that seven-line example matter.

try-with-resources is not optional. Response implements Closeable, and an unclosed response holds its connection. Leak enough and the pool exhausts, at which point requests hang rather than fail — a symptom that looks like a network problem and is not.

body().string() may be called once. It consumes the stream. Calling it twice throws, and calling it after the response is closed throws too. If you need the body more than once, store the string.

execute() is synchronous. For asynchronous work, enqueue() takes a Callback and runs on OkHttp's dispatcher thread pool.

A POST is the same shape with a body:

public static final MediaType JSON = MediaType.get("application/json");

String post(String url, String json) throws IOException {
  RequestBody body = RequestBody.create(json, JSON);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  try (Response response = client.newCall(request).execute()) {
    return response.body().string();
  }
}

Share the Client

The design decision people most often get wrong.

OkHttpClient holds a connection pool and a thread pool. Creating one per request throws away every connection you might have reused, and creates threads you then abandon. It works, it is slow, and under load it exhausts resources.

Create one client for your application and share it. It is thread-safe by design.

When you need different settings for one part of your code, do not build a second client from scratch — clone the existing one so the pools are shared:

OkHttpClient shortTimeout = client.newBuilder()
    .readTimeout(5, TimeUnit.SECONDS)
    .build();

newBuilder() produces a client that shares the connection pool and dispatcher with its parent, which is exactly what you want.

For shutdown, particularly in short-lived processes, release the resources explicitly:

client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();

Without this a JVM may hang for the pool's keep-alive duration before exiting, which is a puzzling thing to debug in a CLI tool.

What OkHttp Adds to Your Request

The library rewrites requests, and knowing what it adds prevents a class of confusion.

The documentation is explicit: "OkHttp may add headers that are absent from the original request, including Content-Length, Transfer-Encoding, User-Agent, Host, Connection, and Content-Type. It will add an Accept-Encoding header for transparent response compression unless the header is already present. If you've got cookies, OkHttp will add a Cookie header with them."

Two consequences.

Transparent compression is automatic and reversed for you. OkHttp requests compression, decompresses the response, and then "will drop the corresponding response headers Content-Encoding and Content-Length because they don't apply to the decompressed response body". So a missing Content-Length on a response is normal rather than a bug — unless you set Accept-Encoding yourself, in which case you own the decompression.

Conditional requests happen automatically when caching is enabled. OkHttp adds If-Modified-Since and If-None-Match to revalidate stale cache entries.

It also follows redirects by default and, on an authorisation challenge, "will ask the Authenticator (if one is configured) to satisfy the challenge", retrying with the supplied credential.

The library describes itself as "principled and avoids being overly configurable, especially when such configuration is to workaround a buggy server, test invalid scenarios or that contradict the relevant RFC". It names its own limitations honestly — it "does not allow GET with a body", and the cache "is not an interface with alternative implementations". If you need to send deliberately invalid requests, this is not the library for it, and that is a design choice rather than an oversight.

Interceptors

The main extension point, and the one feature that will shape how you use the library.

class LoggingInterceptor implements Interceptor {
  @Override public Response intercept(Interceptor.Chain chain) throws IOException {
    Request request = chain.request();
    long t1 = System.nanoTime();
    logger.info(String.format("Sending request %s on %s%n%s",
        request.url(), chain.connection(), request.headers()));

    Response response = chain.proceed(request);

    long t2 = System.nanoTime();
    logger.info(String.format("Received response for %s in %.1fms%n%s",
        response.request().url(), (t2 - t1) / 1e6d, response.headers()));
    return response;
  }
}

The documentation is emphatic that "a call to chain.proceed(request) is a critical part of each interceptor's implementation. This simple-looking method is where all the HTTP work happens." And a warning worth heeding: "if chain.proceed(request) is being called more than once previous response bodies must be closed."

There are two kinds, and choosing correctly matters. Register with addInterceptor() or addNetworkInterceptor(). The documentation sets out the difference precisely.

Application interceptors:

  • "Don't need to worry about intermediate responses like redirects and retries."
  • "Are always invoked once, even if the HTTP response is served from the cache."
  • "Observe the application's original intent. Unconcerned with OkHttp-injected headers like If-None-Match."
  • "Permitted to short-circuit and not call Chain.proceed()."
  • "Permitted to retry and make multiple calls to Chain.proceed()."
  • "Can adjust Call timeouts using withConnectTimeout, withReadTimeout, withWriteTimeout."

Network interceptors:

  • "Able to operate on intermediate responses like redirects and retries."
  • "Not invoked for cached responses that short-circuit the network."
  • "Observe the data just as it will be transmitted over the network."
  • "Access to the Connection that carries the request."

The practical rule: use an application interceptor for anything about your request's intent — adding an authorisation header, a user agent, application-level logging. Use a network interceptor for anything about what actually crosses the wire — inspecting compressed bodies, seeing every redirect hop, examining the connection.

The most common mistake is registering an auth interceptor as a network interceptor, which then fires once per redirect hop and can leak your credential to a host you did not intend.

Timeouts

OkHttp has sensible defaults and four separate settings, and knowing which one fired tells you where the problem is.

OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)
    .callTimeout(60, TimeUnit.SECONDS)
    .build();

connectTimeout covers establishing the TCP and TLS connection. readTimeout applies between individual reads, not to the whole response — so a slow-but-progressing download never trips it. writeTimeout does the same for uploads. callTimeout bounds the entire call including redirects, retries and body transfer. It is zero by default, meaning no overall limit.

That last one is the one to set. Without it, a call that keeps trickling data can run indefinitely, because the read timeout resets on every byte received. callTimeout is the ceiling that makes a job finish, and it is the analogue of --max-time in curl — a distinction we went through in setting a timeout with curl.

Per-call overrides are available through an application interceptor's withReadTimeout and friends, which is how you give one slow endpoint more room without loosening the defaults for everything.

Proxies

Where OkHttp differs from most clients, and where people lose time.

Proxy proxy = new Proxy(Proxy.Type.HTTP,
    new InetSocketAddress("proxy.example.com", 9000));

OkHttpClient client = new OkHttpClient.Builder()
    .proxy(proxy)
    .build();

For SOCKS, use Proxy.Type.SOCKS with the same shape.

Authentication is the part that surprises people. You do not set a Proxy-Authorization header. You supply a proxyAuthenticator, which OkHttp invokes when the proxy issues a 407 challenge:

Authenticator proxyAuth = (route, response) -> {
  if (response.request().header("Proxy-Authorization") != null) {
    return null;   // already tried these credentials; give up
  }
  String credential = Credentials.basic("user", "pass");
  return response.request().newBuilder()
      .header("Proxy-Authorization", credential)
      .build();
};

OkHttpClient client = new OkHttpClient.Builder()
    .proxy(proxy)
    .proxyAuthenticator(proxyAuth)
    .build();

The null return is essential. Without it, a wrong password produces an infinite retry loop rather than a failure — OkHttp asks the authenticator, gets the same bad credential, gets another 407, and asks again. Checking whether you already sent a Proxy-Authorization header is how you break the cycle.

Three further notes.

A 407 is not a 401. The proxy refused you and never reached the target. A 401 means the proxy worked and the target wants credentials, which is a different authenticator() setting entirely.

proxySelector() lets you choose a proxy per request rather than per client, which is how you route different hosts differently without building multiple clients.

Verify it took effect. Request a service that echoes your address, with and without the proxy configured. A misconfiguration here is silent — requests succeed and go directly — and confirming the exit address is the only way to be sure. That silent-success pattern is the one we wrote about in why testing proxies matters.

Handling Responses Properly

A few patterns that make the difference between code that works and code that works under load.

Check isSuccessful(), not just the absence of an exception. OkHttp throws IOException for network failures, not for HTTP error statuses. A 404 or a 500 arrives as an ordinary Response:

try (Response response = client.newCall(request).execute()) {
  if (!response.isSuccessful()) {
    String body = response.body() != null ? response.body().string() : "";
    throw new IOException("HTTP " + response.code() + " from " + request.url()
                          + ": " + body.substring(0, Math.min(200, body.length())));
  }
  return response.body().string();
}

Including the first part of the error body turns an opaque status code into a message that names the problem — most APIs explain themselves in the body of a 400.

Do not buffer large responses into a String. body().string() reads everything into memory. For a large download, stream it:

try (Response response = client.newCall(request).execute();
     BufferedSource source = response.body().source();
     BufferedSink sink = Okio.buffer(Okio.sink(new File("out.bin")))) {
  sink.writeAll(source);
}

Asynchronous calls still need the body closed, and the callback runs on a background thread:

client.newCall(request).enqueue(new Callback() {
  @Override public void onFailure(Call call, IOException e) {
    logger.warn("request failed", e);
  }
  @Override public void onResponse(Call call, Response response) throws IOException {
    try (ResponseBody body = response.body()) {
      handle(body.string());
    }
  }
});

Note that onFailure fires for network problems only. An HTTP 500 arrives in onResponse, which surprises people expecting the naming to mean what it sounds like.

Retries need care. OkHttp retries some connection-level failures automatically, controlled by retryOnConnectionFailure(), which is on by default. It does not retry HTTP error statuses, and it should not — retrying a POST that may have succeeded can duplicate a write. Where you add your own retry logic, do it in an application interceptor, cap the attempts, back off between them, and confine it to idempotent methods unless the API offers an idempotency key.

What Else It Gives You

Briefly, since these are the reasons to choose it.

HTTP/2, where "support allows all requests to the same host to share a socket". Connection pooling, which "reduces request latency (if HTTP/2 isn't available)". Transparent GZIP. Response caching, which "avoids the network completely for repeat requests".

Resilience. It "will silently recover from common connection problems", and where a service has several addresses it "will attempt alternate addresses if the first connect fails" — which the project notes "is necessary for IPv4+IPv6 and services hosted in redundant data centers".

Modern TLS, including TLS 1.3, ALPN and certificate pinning, using the platform's implementation. On the JVM it also supports Conscrypt, which integrates BoringSSL with Java, used automatically if it is the first security provider.

Standards conformance. The project lists the specifications it follows: RFC 9110 for HTTP semantics, RFC 9111 for caching, RFC 9112 for HTTP/1.1, RFC 9113 for HTTP/2, RFC 6455 for WebSockets, and the WHATWG specification for server-sent events. Where a specification is ambiguous, it "follows modern user agents such as popular Browsers or common HTTP Libraries".

People Also Ask

Is OkHttp still maintained?

Yes. The repository has moved from square/okhttp to lysine-dev/okhttp and the documentation site is now at lysine.dev/okhttp, but the project is actively developed and Apache-2.0 licensed. The Maven coordinates remain com.squareup.okhttp3:okhttp.

Should I create a new OkHttpClient for each request?

No. The client holds a connection pool and a thread pool, and it is thread-safe by design. Create one for your application and share it. When you need different settings, use newBuilder() on the existing client so the pools are shared.

Why do I have to close the response?

Because Response holds a connection until closed, and leaked responses exhaust the connection pool. The symptom is requests hanging rather than failing, which is difficult to diagnose. Use try-with-resources, always.

What is the difference between an application and a network interceptor?

An application interceptor sees your original request and is invoked exactly once, even for cached responses, and may short-circuit or retry. A network interceptor sees each individual network exchange including redirects and retries, has access to the connection, and is skipped entirely for cached responses.

How do I set a proxy in OkHttp?

Pass a java.net.Proxy to OkHttpClient.Builder.proxy(). For authentication, set a proxyAuthenticator rather than a header — and return null when a Proxy-Authorization header is already present, or wrong credentials produce an infinite retry loop.

What timeouts should I set?

connectTimeout around 10 seconds, readTimeout and writeTimeout around 30, and — most importantly — a callTimeout, which defaults to zero and is the only setting that bounds the whole call. Without it, a slowly trickling response never times out because the read timeout resets on each byte.

Does OkHttp handle GZIP automatically?

Yes, if you do not set Accept-Encoding yourself. It requests compression, decompresses the response, and drops Content-Encoding and Content-Length because they no longer describe the decompressed body. Set the header manually and you take on decompression too.

What Java version does OkHttp need?

Java 8 or later, and Android 5.0 (API level 21) or later. It depends on Okio and the Kotlin standard library. The old 3.12.x branch supports Java 7 and Android 2.3 but lacks TLS 1.2 support and should not be used.

Wrapping Up

OkHttp is a small API around a well-considered implementation, and two habits cover most of using it correctly: share one client across your application, and close every response with try-with-resources. Both failures are quiet, and both eventually manifest as requests that hang rather than errors you can read.

Interceptors are where you will spend your time, and the application-versus-network distinction is worth learning properly rather than by trial. Application interceptors see your intent and run once; network interceptors see every hop and are skipped for cached responses. Registering an authorisation interceptor at the network layer is the classic mistake, and it fires on every redirect.

Set a callTimeout. It defaults to zero, it is the only setting that bounds a whole call, and its absence is why a job that should take seconds occasionally runs until something else kills it.

And if you are following older documentation, check the links. The project has moved to lysine.dev/okhttp and the Square-hosted site is gone — though the artefact coordinates are unchanged, so your build file needs nothing.

OkHttp in Java: Client Setup Interceptors Timeouts and Proxies | Geonode