• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20176

20 Feb 2026 03:31PM UTC coverage: 88.714% (+0.007%) from 88.707%
#20176

push

github

web-flow
netty: Preserve early server handshake failure cause in logs

Early server-side negotiation failures may terminate a transport before
NettyServerHandler is fully active in the pipeline. In those cases, the
original handshake failure can be missing from transport termination
logging because termination may rely on connectionError(), which can be
null on this early path.

This change adds a server-side NOOP write in
NettyServerTransport.start() (analogous to the existing client-side NOOP
write path). If that write fails, its cause is passed to
notifyTerminated(), preserving and logging the original transport
termination reason for debugging.

To support this, NettyServerHandler now accepts NOOP_MESSAGE writes by
writing an empty buffer, and tests are added to verify:
  - transport failure logging for plaintext-client to TLS-server failure
  - server NOOP write handling in NettyServerHandler

Fixes #8495

35459 of 39970 relevant lines covered (88.71%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.55
/../netty/src/main/java/io/grpc/netty/NettyServerTransport.java
1
/*
2
 * Copyright 2014 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.netty;
18

19
import com.google.common.annotations.VisibleForTesting;
20
import com.google.common.base.MoreObjects;
21
import com.google.common.base.Preconditions;
22
import com.google.common.collect.ImmutableList;
23
import com.google.common.util.concurrent.ListenableFuture;
24
import com.google.common.util.concurrent.SettableFuture;
25
import io.grpc.Attributes;
26
import io.grpc.InternalChannelz.SocketStats;
27
import io.grpc.InternalLogId;
28
import io.grpc.ServerStreamTracer;
29
import io.grpc.Status;
30
import io.grpc.internal.ServerTransport;
31
import io.grpc.internal.ServerTransportListener;
32
import io.grpc.internal.TransportTracer;
33
import io.netty.channel.Channel;
34
import io.netty.channel.ChannelFuture;
35
import io.netty.channel.ChannelFutureListener;
36
import io.netty.channel.ChannelHandler;
37
import io.netty.channel.ChannelPromise;
38
import io.netty.util.concurrent.Future;
39
import io.netty.util.concurrent.GenericFutureListener;
40
import java.io.IOException;
41
import java.net.SocketAddress;
42
import java.net.SocketException;
43
import java.util.List;
44
import java.util.concurrent.ScheduledExecutorService;
45
import java.util.logging.Level;
46
import java.util.logging.Logger;
47

48
/**
49
 * The Netty-based server transport.
50
 */
51
class NettyServerTransport implements ServerTransport {
52
  // connectionLog is for connection related messages only
53
  private static final Logger connectionLog = Logger.getLogger(
1✔
54
      String.format("%s.connections", NettyServerTransport.class.getName()));
1✔
55

56
  // Some exceptions are not very useful and add too much noise to the log
57
  private static final ImmutableList<String> QUIET_EXCEPTIONS = ImmutableList.of(
1✔
58
      "NativeIoException" /* Netty exceptions */);
59

60
  private final InternalLogId logId;
61
  private final Channel channel;
62
  private final ChannelPromise channelUnused;
63
  private final ProtocolNegotiator protocolNegotiator;
64
  private final int maxStreams;
65
  // only accessed from channel event loop
66
  private NettyServerHandler grpcHandler;
67
  private ServerTransportListener listener;
68
  private boolean terminated;
69
  private final boolean autoFlowControl;
70
  private final int flowControlWindow;
71
  private final int maxMessageSize;
72
  private final int maxHeaderListSize;
73
  private final int softLimitHeaderListSize;
74
  private final long keepAliveTimeInNanos;
75
  private final long keepAliveTimeoutInNanos;
76
  private final long maxConnectionIdleInNanos;
77
  private final long maxConnectionAgeInNanos;
78
  private final long maxConnectionAgeGraceInNanos;
79
  private final boolean permitKeepAliveWithoutCalls;
80
  private final long permitKeepAliveTimeInNanos;
81
  private final int maxRstCount;
82
  private final long maxRstPeriodNanos;
83
  private final Attributes eagAttributes;
84
  private final List<? extends ServerStreamTracer.Factory> streamTracerFactories;
85
  private final TransportTracer transportTracer;
86

87
  NettyServerTransport(
88
      Channel channel,
89
      ChannelPromise channelUnused,
90
      ProtocolNegotiator protocolNegotiator,
91
      List<? extends ServerStreamTracer.Factory> streamTracerFactories,
92
      TransportTracer transportTracer,
93
      int maxStreams,
94
      boolean autoFlowControl,
95
      int flowControlWindow,
96
      int maxMessageSize,
97
      int maxHeaderListSize,
98
      int softLimitHeaderListSize,
99
      long keepAliveTimeInNanos,
100
      long keepAliveTimeoutInNanos,
101
      long maxConnectionIdleInNanos,
102
      long maxConnectionAgeInNanos,
103
      long maxConnectionAgeGraceInNanos,
104
      boolean permitKeepAliveWithoutCalls,
105
      long permitKeepAliveTimeInNanos,
106
      int maxRstCount,
107
      long maxRstPeriodNanos,
108
      Attributes eagAttributes) {
1✔
109
    this.channel = Preconditions.checkNotNull(channel, "channel");
1✔
110
    this.channelUnused = channelUnused;
1✔
111
    this.protocolNegotiator = Preconditions.checkNotNull(protocolNegotiator, "protocolNegotiator");
1✔
112
    this.streamTracerFactories =
1✔
113
        Preconditions.checkNotNull(streamTracerFactories, "streamTracerFactories");
1✔
114
    this.transportTracer = Preconditions.checkNotNull(transportTracer, "transportTracer");
1✔
115
    this.maxStreams = maxStreams;
1✔
116
    this.autoFlowControl = autoFlowControl;
1✔
117
    this.flowControlWindow = flowControlWindow;
1✔
118
    this.maxMessageSize = maxMessageSize;
1✔
119
    this.maxHeaderListSize = maxHeaderListSize;
1✔
120
    this.softLimitHeaderListSize = softLimitHeaderListSize;
1✔
121
    this.keepAliveTimeInNanos = keepAliveTimeInNanos;
1✔
122
    this.keepAliveTimeoutInNanos = keepAliveTimeoutInNanos;
1✔
123
    this.maxConnectionIdleInNanos = maxConnectionIdleInNanos;
1✔
124
    this.maxConnectionAgeInNanos = maxConnectionAgeInNanos;
1✔
125
    this.maxConnectionAgeGraceInNanos = maxConnectionAgeGraceInNanos;
1✔
126
    this.permitKeepAliveWithoutCalls = permitKeepAliveWithoutCalls;
1✔
127
    this.permitKeepAliveTimeInNanos = permitKeepAliveTimeInNanos;
1✔
128
    this.maxRstCount = maxRstCount;
1✔
129
    this.maxRstPeriodNanos = maxRstPeriodNanos;
1✔
130
    this.eagAttributes = Preconditions.checkNotNull(eagAttributes, "eagAttributes");
1✔
131
    SocketAddress remote = channel.remoteAddress();
1✔
132
    this.logId = InternalLogId.allocate(getClass(), remote != null ? remote.toString() : null);
1✔
133
  }
1✔
134

135
  public void start(ServerTransportListener listener) {
136
    Preconditions.checkState(this.listener == null, "Handler already registered");
1✔
137
    this.listener = listener;
1✔
138

139
    // Create the Netty handler for the pipeline.
140
    grpcHandler = createHandler(listener, channelUnused);
1✔
141

142
    // Notify when the channel closes.
143
    final class TerminationNotifier implements ChannelFutureListener {
1✔
144
      boolean done;
145

146
      @Override
147
      public void operationComplete(ChannelFuture future) throws Exception {
148
        if (!done) {
1✔
149
          done = true;
1✔
150
          notifyTerminated(grpcHandler.connectionError());
1✔
151
        }
152
      }
1✔
153
    }
154

155
    ChannelHandler negotiationHandler = protocolNegotiator.newHandler(grpcHandler);
1✔
156
    ChannelHandler bufferingHandler = new WriteBufferingAndExceptionHandler(negotiationHandler);
1✔
157

158
    ChannelFutureListener terminationNotifier = new TerminationNotifier();
1✔
159
    channelUnused.addListener(terminationNotifier);
1✔
160
    channel.closeFuture().addListener(terminationNotifier);
1✔
161

162
    channel.pipeline().addLast(bufferingHandler);
1✔
163

164
    channel.writeAndFlush(NettyServerHandler.NOOP_MESSAGE).addListener(new ChannelFutureListener() {
1✔
165
      @Override
166
      public void operationComplete(ChannelFuture future) throws Exception {
167
        if (!future.isSuccess()) {
1✔
168
          // grpcHandler (NettyServerHandler) may not be in the pipeline yet on early negotiation
169
          // failure, so connectionError() can remain null. Notify termination here with the write
170
          // failure cause to preserve/log the original transport termination reason.
171
          notifyTerminated(future.cause());
1✔
172
        }
173
      }
1✔
174
    });
175
  }
1✔
176

177
  @Override
178
  public ScheduledExecutorService getScheduledExecutorService() {
179
    return channel.eventLoop();
1✔
180
  }
181

182
  @Override
183
  public void shutdown() {
184
    if (channel.isOpen()) {
1✔
185
      channel.close();
1✔
186
    }
187
  }
1✔
188

189
  @Override
190
  public void shutdownNow(Status reason) {
191
    if (channel.isOpen()) {
1✔
192
      channel.writeAndFlush(new ForcefulCloseCommand(reason));
1✔
193
    }
194
  }
1✔
195

196
  @Override
197
  public InternalLogId getLogId() {
198
    return logId;
1✔
199
  }
200

201
  /**
202
   * For testing purposes only.
203
   */
204
  Channel channel() {
205
    return channel;
1✔
206
  }
207

208
  /**
209
   * Accepts a throwable and returns the appropriate logging level. Uninteresting exceptions
210
   * should not clutter the log.
211
   */
212
  @VisibleForTesting
213
  static Level getLogLevel(Throwable t) {
214
    if (t.getClass().equals(IOException.class)
1✔
215
        || t.getClass().equals(SocketException.class)
1✔
216
        || QUIET_EXCEPTIONS.contains(t.getClass().getSimpleName())) {
1✔
217
      return Level.FINE;
1✔
218
    }
219
    return Level.INFO;
1✔
220
  }
221

222
  private void notifyTerminated(Throwable t) {
223
    if (t != null) {
1✔
224
      connectionLog.log(getLogLevel(t), "Transport failed", t);
1✔
225
    }
226
    if (!terminated) {
1✔
227
      terminated = true;
1✔
228
      listener.transportTerminated();
1✔
229
    }
230
  }
1✔
231

232
  @Override
233
  public ListenableFuture<SocketStats> getStats() {
234
    final SettableFuture<SocketStats> result = SettableFuture.create();
1✔
235
    if (channel.eventLoop().inEventLoop()) {
1✔
236
      // This is necessary, otherwise we will block forever if we get the future from inside
237
      // the event loop.
238
      result.set(getStatsHelper(channel));
×
239
      return result;
×
240
    }
241
    channel.eventLoop().submit(
1✔
242
        new Runnable() {
1✔
243
          @Override
244
          public void run() {
245
            result.set(getStatsHelper(channel));
1✔
246
          }
1✔
247
        })
248
        .addListener(
1✔
249
            new GenericFutureListener<Future<Object>>() {
1✔
250
              @Override
251
              public void operationComplete(Future<Object> future) throws Exception {
252
                if (!future.isSuccess()) {
1✔
253
                  result.setException(future.cause());
×
254
                }
255
              }
1✔
256
            });
257
    return result;
1✔
258
  }
259

260
  private SocketStats getStatsHelper(Channel ch) {
261
    Preconditions.checkState(ch.eventLoop().inEventLoop());
1✔
262
    return new SocketStats(
1✔
263
        transportTracer.getStats(),
1✔
264
        channel.localAddress(),
1✔
265
        channel.remoteAddress(),
1✔
266
        Utils.getSocketOptions(ch),
1✔
267
        grpcHandler == null ? null : grpcHandler.getSecurityInfo());
1✔
268

269
  }
270

271
  @Override
272
  public String toString() {
273
    return MoreObjects.toStringHelper(this)
×
274
        .add("logId", logId.getId())
×
275
        .add("channel", channel)
×
276
        .toString();
×
277
  }
278

279
  /**
280
   * Creates the Netty handler to be used in the channel pipeline.
281
   */
282
  private NettyServerHandler createHandler(
283
      ServerTransportListener transportListener, ChannelPromise channelUnused) {
284
    return NettyServerHandler.newHandler(
1✔
285
        transportListener,
286
        channelUnused,
287
        streamTracerFactories,
288
        transportTracer,
289
        maxStreams,
290
        autoFlowControl,
291
        flowControlWindow,
292
        maxHeaderListSize,
293
        softLimitHeaderListSize,
294
        maxMessageSize,
295
        keepAliveTimeInNanos,
296
        keepAliveTimeoutInNanos,
297
        maxConnectionIdleInNanos,
298
        maxConnectionAgeInNanos,
299
        maxConnectionAgeGraceInNanos,
300
        permitKeepAliveWithoutCalls,
301
        permitKeepAliveTimeInNanos,
302
        maxRstCount,
303
        maxRstPeriodNanos,
304
        eagAttributes);
305
  }
306
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc