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)
Mantenedores costumam responder em até 1 dia
Avaliação
- Dificuldade
- 2/5
- Tempo estimado
- 1-3 horas
- Facilidade para iniciantes
- 78/100
Direção de pesquisa
Comece em okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java, em FrameHandler.run(), ao redor da chamada a readConnectionPreface() e do bloco catch (Throwable t) que registra em WARNING. Execute o teste de reprodução OkHttpServerTransportEofTest em okhttp/src/test/java/io/grpc/okhttp, que atualmente falha por causa do aviso. Está concluído quando um EOF limpo antes do preface é registrado em FINE ou não é registrado, o teste passa e outros erros de I/O continuam sendo registrados como antes.
Escrita pelo modelo de indexação a partir do texto da issue.
Descrição
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.
- Linguagem predominante
- Java
- Estrelas
- 12.1k
- Forks
- 4k
- Merge médio
- 1d 13h
- PRs com merge (30d)
- 42
Preparar o ambiente
- Sem Dockerfile nem arquivo Docker Compose
- Sem modelo de pull request
- Ler o guia de contribuição
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Mais de grpc/grpc-java
-
alts: AuthorizationUtil.clientAuthorizationCheck compares the peer service account case-sensitivelyAberta
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 85/100
grpc/grpc-java#13156 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
grpc/grpc-java#13155 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 82/100
grpc/grpc-java#13154 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
xds: EDS resource is not NACKed when locality weights at one priority sum to more than uint32 maxAbertabug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
grpc/grpc-java#13153 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
-
xds: RBAC `Authenticated` principal without `principal_name` is rejected ("Unknown StringMatcher match pattern: MATCHPATTERN_NOT_SET")Talvez já em andamento @hidingmyname assumiu hoje. Abertabug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 78/100
grpc/grpc-java#13150 · 1 comentário ·
Mantenedores costumam responder em até 1 dia
Todas as issues de grpc/grpc-java
Issues semelhantes
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 66/100
a2aproject/a2a-java#1275 ·
Mantenedores costumam responder em até 1 dia
-
module/ftp Type/Bug
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
ballerina-platform/ballerina-library#9476 ·
Mantenedores costumam responder em até 1 dia
-
[Feature] 为世界管理页面添加打开存档文件夹按钮Abertaenhancement
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 65/100
HMCL-dev/HMCL#7175 · 1 reação ·
Mantenedores costumam responder em até 1 dia
-
enhancement
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
anwilli5/coin-collection-android-US#515 ·
Mantenedores costumam responder em até 1 dia
-
Dificuldade 2/5 1-3 horas Facilidade para iniciantes 72/100
FasterXML/jackson-databind#6398 ·
Mantenedores costumam responder em até 1 dia