Skip to content

Commit eaad084

Browse files
authored
Suppress fcntl errors on the listener's accept path (#191)
Motivation: On Darwin, 'fcntl(2)' on a just-accepted socket can fail with EINVAL, which NIO surfaces as a 'NIOFcntlFailedError'. NIO closes the socket directly and fires the error down the pipeline for informational purposes. However, when wrapped in a NIOAsyncChannel channel the read loop throws this error which causes the server to shutdown. Note that testing this reliably is difficult: the error type is public but its init is not so we cannot synthesize the failure. Hitting this is possible in release builds after serveral thousand iterations but much less so in debug builds. Modifications: - Ignore the error on Darwin Result: Fewer errors
1 parent c46f77c commit eaad084

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

Sources/GRPCNIOTransportHTTP2Posix/HTTP2ServerTransport+Posix.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,15 @@ extension HTTP2ServerTransport {
126126
let serverChannel = try await ServerBootstrap(group: self.eventLoopGroup)
127127
.serverChannelOption(.socketOption(.so_reuseaddr), value: 1)
128128
.serverChannelInitializer { channel in
129+
#if canImport(Darwin)
130+
channel.eventLoop.makeCompletedFuture {
131+
try channel.pipeline.syncOperations.addHandler(SwallowFcntlFailedErrorHandler())
132+
}.flatMap {
133+
listenerConfigurator.configure(channel: channel)
134+
}
135+
#else
129136
listenerConfigurator.configure(channel: channel)
137+
#endif
130138
}
131139
.bind(to: self.address) { channel in
132140
channel.eventLoop.makeCompletedFuture {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2026, gRPC Authors All rights reserved.
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+
internal import NIOCore
18+
internal import NIOPosix
19+
20+
/// A handler for the listening channel which drops `NIOFcntlFailedError`s.
21+
///
22+
/// On Darwin, `fcntl(2)` on a just-accepted socket whose peer has already closed can fail with
23+
/// `EINVAL`, which NIO surfaces as a `NIOFcntlFailedError`. NIO treats this as recoverable and
24+
/// keeps the listening channel open but still fires the error down the pipeline. The socket
25+
/// which failed to be configured has already been closed by NIO and no `Channel` was created
26+
/// for it: the error is is purely informational.
27+
///
28+
/// When the listening channel is wrapped in a `NIOAsyncChannel`: its handler finishes the
29+
/// stream of accepted connections with _any_ error it catches.
30+
final class SwallowFcntlFailedErrorHandler: ChannelInboundHandler {
31+
typealias InboundIn = Any
32+
typealias InboundOut = Any
33+
34+
func errorCaught(context: ChannelHandlerContext, error: any Error) {
35+
switch error {
36+
case is NIOFcntlFailedError:
37+
() // Okay, the accepted socket has already been closed by NIO.
38+
default:
39+
context.fireErrorCaught(error)
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)