Skip to content

Commit 35a5e0d

Browse files
committed
fix(quic): only catch Exception, not fatal Error, on inbound rejection
Narrow the inbound rejection guard from catch(Throwable) to catch(Exception) so fatal Errors (OutOfMemoryError, LinkageError, ...) propagate instead of being downgraded to a routine connection close. The library cannot distinguish a deliberate rejection from an application bug, so logging stays at debug to avoid reintroducing noise.
1 parent 16351a6 commit 35a5e0d

2 files changed

Lines changed: 32 additions & 2 deletions

File tree

libp2p/src/main/kotlin/io/libp2p/transport/quic/QuicTransport.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,15 @@ class QuicTransport(
137137
// pipeline's channelActive, and the handshake-waiter handler has already removed itself
138138
// by this point, so an escaping exception would reach the Netty pipeline tail and log a
139139
// noisy "exceptionCaught reached tail of pipeline" warning while leaking the channel.
140-
// Close the rejected connection instead, mirroring the dial paths.
140+
// Close the rejected connection instead, mirroring the dial paths. Only Exception is
141+
// caught — fatal Errors (OutOfMemoryError, LinkageError, ...) must propagate rather than
142+
// be downgraded to a routine rejection. The library cannot tell a deliberate rejection
143+
// from an application bug (the handler contract has no typed rejection), so the log stays
144+
// at debug to avoid reintroducing the noise this guard removes; the application owns
145+
// logging of its own connection decisions.
141146
try {
142147
exposeConnection()
143-
} catch (e: Throwable) {
148+
} catch (e: Exception) {
144149
logger.debug("Inbound connection rejected by handler; closing", e)
145150
closeChannel()
146151
}

libp2p/src/test/kotlin/io/libp2p/transport/quic/QuicInboundHandshakeRoutingTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.mockk.mockk
77
import io.netty.handler.codec.quic.QuicChannel
88
import org.assertj.core.api.Assertions.assertThat
99
import org.assertj.core.api.Assertions.assertThatCode
10+
import org.assertj.core.api.Assertions.assertThatThrownBy
1011
import org.junit.jupiter.api.Test
1112
import java.util.concurrent.CompletableFuture
1213

@@ -136,4 +137,28 @@ class QuicInboundHandshakeRoutingTest {
136137

137138
assertThat(closed).isTrue()
138139
}
140+
141+
@Test
142+
fun `does not swallow a fatal Error thrown by the handler`() {
143+
val transport = transport()
144+
val inbound = identity()
145+
146+
var closed = false
147+
148+
// A fatal Error (OutOfMemoryError, LinkageError, ...) is NOT a routine connection rejection
149+
// and must propagate rather than be silently downgraded to a closed connection. Only
150+
// Exception is caught by routeInboundHandshake.
151+
assertThatThrownBy {
152+
transport.routeInboundHandshake(
153+
remoteIdentity = inbound,
154+
pending = null,
155+
prepareConnection = { },
156+
closeChannel = { closed = true },
157+
holePunchChannel = { error("no hole punch is pending") },
158+
exposeConnection = { throw OutOfMemoryError("simulated fatal error") }
159+
)
160+
}.isInstanceOf(OutOfMemoryError::class.java)
161+
162+
assertThat(closed).isFalse()
163+
}
139164
}

0 commit comments

Comments
 (0)