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)

Aberta Para iniciantes
#13,152 1 comentário 0 reações 0 responsáveis Ver no GitHub

Mantenedores costumam responder em até 1 dia

@hidingmyname já está trabalhando nisso.

Desde 2/11/2026.

  • #13162 de @hidingmyname — aberto

Avaliação

Dificuldade
2/5
Tempo estimado
1-3 horas
Facilidade para iniciantes
78/100
Tipo de issue
Bug
Clareza
Claramente especificada
Status de atividade
Ativa
Stack de tecnologia
grpc, java
Domínio
backend

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

      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

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Mais de grpc/grpc-java

Todas as issues de grpc/grpc-java

Issues semelhantes

Mais issues de Java

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.