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)
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 78/100
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):
okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java:585-587:FrameHandler.run()reads the connection preface.okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java:613-616: anyThrowable, includingEOFException, is logged atWARNING.okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java:109-115:readConnectionPreface()lets theEOFExceptionfrom okio propagate. By contrast,nextFrame()(okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java:117-122) treats anIOExceptionon the frame header as a normal socket close.
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
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de grpc/grpc-java
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 88/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
alts: AuthorizationUtil.clientAuthorizationCheck compares the peer service account case-sensitivelyPosiblemente ocupada @hidingmyname la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
grpc/grpc-java#13156 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Binary log config: `-service/*` exclusion is accepted and silently turned into a per-service rule, so the service is still loggedPosiblemente ocupada @hidingmyname la tomó hoy. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
grpc/grpc-java#13155 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
SpiffeUtil.loadTrustBundleFromFile accepts invalid trust domain names (e.g. `UPPER.example.com`, `invalid#character`) as bundle map keysPosiblemente ocupada @hidingmyname la tomó hoy. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
grpc/grpc-java#13154 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de grpc/grpc-java
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
micronaut-projects/micronaut-redis#844 ·
Los mantenedores suelen responder en 4 días
-
ai-driven-fuzzing bug claude Major
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
linagora/tmail-backend#2862 ·
Los mantenedores suelen responder en 1 día
-
ai-driven-fuzzing bug claude Minor
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
linagora/twake-calendar-side-service#1435 ·
Los mantenedores suelen responder en 1 día
-
Poetry updater rejects OR (||) Python constraints in PyprojectPreparer#update_python_requirementAbiertoL: python L: ruby:bundler
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
dependabot/dependabot-core#16819 ·
Los mantenedores suelen responder en 1 día
-
[Bug] Auto-trusted serialization allow list skips array component classesPosiblemente ocupada @Icesource la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día