okhttp: OkHttpServerTransport logs a WARNING with EOFException for every TCP connection that closes before sending the HTTP/2 preface (e.g. load balancer health checks)

Abierto Apto para principiantes
#13,152 1 comentario 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@hidingmyname ya está trabajando en esto.

Desde el 2/11/2026.

  • #13162 de @hidingmyname — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
78/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
grpc, java
Área
backend

Línea de trabajo

Empieza en okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java, en FrameHandler.run() alrededor de la llamada a readConnectionPreface() y del bloque catch (Throwable t) que registra en WARNING. Ejecuta la prueba de reproducción OkHttpServerTransportEofTest en okhttp/src/test/java/io/grpc/okhttp, que actualmente falla por la advertencia. Está terminado cuando un EOF limpio antes del preface se registra en FINE o no se registra, la prueba pasa y los demás errores de I/O siguen registrándose como antes.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

What version of gRPC-Java are you using?

master at 15d64a4 (1.86.0-SNAPSHOT). The code involved is unchanged on the current master.

What is your environment?

Linux 5.15, OpenJDK 21.0.2, Gradle 8.14.5 (wrapper).

What did you expect to see?

When a client opens a TCP connection to an OkHttp-based gRPC server and closes it without sending any bytes, the server should close the transport quietly, or log at most at FINE. L4 load balancers and Kubernetes TCP probes do this on every health check.

What did you see instead?

Each such connection logs a WARNING with a full stack trace:

Oct 31, 2026 7:44:27 PM io.grpc.okhttp.OkHttpServerTransport$FrameHandler run
WARNING: Error decoding HTTP/2 frames
java.io.EOFException
	at okio.RealBufferedSource.require(RealBufferedSource.kt:202)
	at okio.RealBufferedSource.readByteString(RealBufferedSource.kt:218)
	at io.grpc.okhttp.internal.framed.Http2$Reader.readConnectionPreface(Http2.java:111)
	at io.grpc.okhttp.OkHttpServerTransport$FrameHandler.run(OkHttpServerTransport.java:586)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)

Location (grpc/grpc-java @ 15d64a4):

      try {
        frameReader.readConnectionPreface();
        ...
      } catch (Throwable t) {
        log.log(Level.WARNING, "Error decoding HTTP/2 frames", t);
        abruptShutdown(ErrorCode.INTERNAL_ERROR, "Error in frame decoder",
            Status.INTERNAL.withDescription("Error decoding HTTP/2 frames").withCause(t), false);

Problem

A clean EOF before the preface is expected traffic, not a decoding error, but it goes through the generic catch (Throwable t) and is logged at WARNING. With several nodes and short health-check intervals, these warnings can fill the server logs. This only covers a clean EOF before the preface. Connection resets and other I/O errors are not affected, and apart from the log, the transport behaves the same (it shuts down and terminates).

Reproduction

OkHttpServerTransportEofTest.java (okhttp/src/test/java/io/grpc/okhttp)
/*
 * Copyright 2024 The gRPC Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.grpc.okhttp;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;

import io.grpc.Attributes;
import io.grpc.internal.FakeClock;
import io.grpc.internal.ServerTransportListener;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Tests for EOF handling in {@link OkHttpServerTransport} when cloud load
 * balancers perform TCP health checks (connect then immediately disconnect).
 */
@RunWith(JUnit4.class)
public class OkHttpServerTransportEofTest {
  private static final int TIME_OUT_MS = 5000;

  private final ExecutorService threadPool = Executors.newCachedThreadPool();
  private final FakeClock fakeClock = new FakeClock();
  private Socket clientSocket;
  private Socket serverSocket;

  @Rule public final Timeout globalTimeout = Timeout.seconds(10);

  @After
  public void tearDown() throws Exception {
    threadPool.shutdownNow();
    if (clientSocket != null && !clientSocket.isClosed()) {
      clientSocket.close();
    }
    if (serverSocket != null && !serverSocket.isClosed()) {
      serverSocket.close();
    }
  }

  /**
   * When a cloud load balancer performs a TCP health check by opening and immediately
   * closing a connection (sending no data), the server should handle the resulting EOF
   * gracefully without producing WARNING-level log messages.
   */
  @Test
  public void immediateClientClose_shouldNotProduceWarningLog() throws Exception {
    // Capture WARNING-level logs from OkHttpServerTransport
    Logger transportLogger = Logger.getLogger(OkHttpServerTransport.class.getName());
    Level originalLevel = transportLogger.getLevel();
    transportLogger.setLevel(Level.ALL);
    List<String> warningLogs = Collections.synchronizedList(new ArrayList<>());
    Handler logHandler = new Handler() {
      @Override
      public void publish(LogRecord record) {
        if (record.getLevel().intValue() >= Level.WARNING.intValue()) {
          warningLogs.add(
              record.getLevel() + ": " + record.getMessage() + ": " + record.getThrown());
        }
      }

      @Override
      public void flush() {}

      @Override
      public void close() {}
    };
    transportLogger.addHandler(logHandler);

    try {
      // Create a real TCP socket pair
      try (ServerSocket listenSocket = new ServerSocket(0)) {
        Future<Socket> serverFuture = threadPool.submit(() -> listenSocket.accept());
        clientSocket = new Socket();
        clientSocket.connect(listenSocket.getLocalSocketAddress());
        serverSocket = serverFuture.get();
      }

      // Set up the OkHttp server transport
      HandshakerSocketFactory handshakerSocketFactory = new PlaintextHandshakerSocketFactory();
      OkHttpServerBuilder serverBuilder = new OkHttpServerBuilder(
              new InetSocketAddress(1234), handshakerSocketFactory)
          .executor(fakeClock.getScheduledExecutorService())
          .scheduledExecutorService(fakeClock.getScheduledExecutorService())
          .transportExecutor(threadPool);

      ServerTransportListener transportListener = mock(ServerTransportListener.class);

      OkHttpServerTransport serverTransport = new OkHttpServerTransport(
          new OkHttpServerTransport.Config(serverBuilder, Arrays.asList()),
          serverSocket);
      serverTransport.start(transportListener);

      // Simulate load balancer health check: connect and immediately disconnect
      clientSocket.close();

      // Wait for the transport to terminate
      verify(transportListener, timeout(TIME_OUT_MS)).transportTerminated();

      // Transport should not have become ready since no HTTP/2 handshake occurred
      verify(transportListener, never()).transportReady(any(Attributes.class));

      // An immediate client disconnect (EOF) should be handled gracefully without
      // WARNING-level log output. Cloud load balancers perform regular TCP health
      // checks that open and close connections without sending data, and these should
      // not produce noisy log messages.
      assertThat(warningLogs).isEmpty();
    } finally {
      transportLogger.removeHandler(logHandler);
      transportLogger.setLevel(originalLevel);
    }
  }
}

Run: ./gradlew :grpc-okhttp:test --tests io.grpc.okhttp.OkHttpServerTransportEofTest -PskipCodegen=true -PskipAndroid=true

OkHttpServerTransportEofTest > immediateClientClose_shouldNotProduceWarningLog FAILED
    expected to be empty
    but was: [WARNING: Error decoding HTTP/2 frames: java.io.EOFException]
        at app//io.grpc.okhttp.OkHttpServerTransportEofTest.immediateClientClose_shouldNotProduceWarningLog(OkHttpServerTransportEofTest.java:142)

1 test completed, 1 failed

Environment: commit 15d64a4, OpenJDK 21.0.2, Gradle 8.14.5.

Already fixed in grpc/grpc-go

PR: https://github.com/grpc/grpc-go/pull/4458, "internal/transport: skip log on EOF when reading client preface" (merged 2021-06-02T23:47:35Z, merge commit 174b1c28afa). grpc-go is another gRPC server implementation. It does not log when the connection gets io.EOF before the client preface, and gives the same health-check reason.

How grpc-go fixed it (relevant hunks, verbatim):

--- a/internal/transport/http2_server.go
+++ b/internal/transport/http2_server.go
@@ -266,6 +271,13 @@ func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err
 	// Check the validity of client preface.
 	preface := make([]byte, len(clientPreface))
 	if _, err := io.ReadFull(t.conn, preface); err != nil {
+		// In deployments where a gRPC server runs behind a cloud load balancer
+		// which performs regular TCP level health checks, the connection is
+		// closed immediately by the latter. Skipping the error here will help
+		// reduce log clutter.
+		if err == io.EOF {
+			return nil, nil
+		}
 		return nil, connectionErrorf(false, err, "transport: http2Server.HandleStreams failed to receive the preface from client: %v", err)
 	}
 	if !bytes.Equal(preface, clientPreface) {
Suggested fix

Catch EOFException around readConnectionPreface(), log it at FINE, and shut the transport down the same way as before:

diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java
--- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java
+++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java
@@ -50,6 +50,7 @@ import io.grpc.okhttp.internal.framed.HeadersMode;
 import io.grpc.okhttp.internal.framed.Http2;
 import io.grpc.okhttp.internal.framed.Settings;
 import io.grpc.okhttp.internal.framed.Variant;
+import java.io.EOFException;
 import java.io.IOException;
 import java.net.Socket;
 import java.net.SocketException;
@@ -583,7 +584,17 @@ final class OkHttpServerTransport implements ServerTransport,
       String threadName = Thread.currentThread().getName();
       Thread.currentThread().setName("OkHttpServerTransport");
       try {
-        frameReader.readConnectionPreface();
+        try {
+          frameReader.readConnectionPreface();
+        } catch (EOFException e) {
+          // The client closed the connection before sending the preface, e.g. a TCP health check
+          // from a load balancer. This is expected, so don't log it as a warning.
+          log.log(Level.FINE, "Connection closed before receiving the connection preface", e);
+          abruptShutdown(ErrorCode.INTERNAL_ERROR, "EOF while reading connection preface",
+              Status.UNAVAILABLE.withDescription("Connection closed before connection preface"),
+              false);
+          return;
+        }
         if (!frameReader.nextFrame(this)) {
           connectionError(ErrorCode.INTERNAL_ERROR, "Failed to read initial SETTINGS");
           return;

Verified on 15d64a4 (OpenJDK 21.0.2, Gradle 8.14.5): the test above fails without this patch and passes with it; no other test results change (:grpc-okhttp:test, 364 tests).

I can submit a PR for this.

I searched issues and PRs for OkHttp server EOF/connection-preface handling and for the "Error decoding HTTP/2 frames" warning, and found no existing report.

Lenguaje dominante
Java
Estrellas
12.1k
Forks
4k
Merge medio
1 d 20 h
PR fusionados (30 d)
43

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de grpc/grpc-java

Todos los issues de grpc/grpc-java

Issues similares

Más issues de Java

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.