Skip to content

fix: avoid NPE in outbound TCP stage when upstream finishes before connect (#3255)#3259

Draft
He-Pin wants to merge 1 commit into
mainfrom
fix/3255-tcp-halfclose-npe
Draft

fix: avoid NPE in outbound TCP stage when upstream finishes before connect (#3255)#3259
He-Pin wants to merge 1 commit into
mainfrom
fix/3255-tcp-halfclose-npe

Conversation

@He-Pin

@He-Pin He-Pin commented Jun 28, 2026

Copy link
Copy Markdown
Member

Motivation

TcpConnectionStage.closeConnectionUpstreamFinished() dereferences the
connection ActorRef without a null check on the full-close path. For an
outbound connection whose upstream finishes before the Connected
message arrives, connection is still null (it is only assigned when
Connected is received), so connection ! Close throws a
NullPointerException and fails the stage.

This is easy to hit with a source that completes eagerly: Source.empty
completes synchronously during materialization, i.e. before the asynchronous
Connected reply, so onUpstreamFinish runs while connection is null.
The existing null check only guarded the half-close (ConfirmedClose) path,
leaving the halfClose = false path unprotected.

Additionally, the connecting handler always issued ConfirmedClose (a
half-close) regardless of the halfClose setting when the upstream had
already finished before the connection was established — incorrect when
half-close is disabled, because it would keep the read side open instead of
fully tearing the connection down.

Fixes #3255.

Modification

  • Hoist a connection eq null guard to the top of
    closeConnectionUpstreamFinished() so neither close branch can dereference
    a null connection. When the connection is not yet established the close is
    deferred until it is — this mirrors the structure already used by the
    sibling closeConnectionDownstreamFinished().
  • Make the connecting handler honor the half-close setting when the upstream
    finished before connect: send Close when half-close is disabled and
    ConfirmedClose when it is enabled.
  • Added explanatory comments at both sites so the deferral/half-close
    reasoning is clear to future readers.

Result

  • Outbound connections with halfClose = false no longer throw a
    NullPointerException when the upstream finishes before the connection is
    established.
  • The early-finish close path now respects the configured half-close
    semantics (Close for full-close, ConfirmedClose for half-close).
  • All touched symbols are private[stream] / @InternalApi internals, so
    there is no public API or binary-compatibility change (no MiMa filter
    required).

Tests

  • sbt "stream-tests/testOnly org.apache.pekko.stream.io.TcpSpec" → 28 passed.
  • Added a directional regression test
    (TcpSpec: "not throw a NullPointerException when full-close is requested
    and upstream finishes before the connection is established"). It fails
    with the exact NullPointerException before the fix
    (verified by
    temporarily reverting the source change) and passes after.

References

Fixes #3255


This is an original contribution to Apache Pekko, made under the Apache
License 2.0 (i.e. the changes are now Apache licensed). No third-party or
Akka-derived code is included.

…nnect

Motivation:
TcpConnectionStage.closeConnectionUpstreamFinished() dereferenced the
connection ActorRef without a null check on the half-close-disabled path.
For an outbound connection whose upstream finishes before the Connected
message arrives, connection is still null, so connection ! Close threw a
NullPointerException and failed the stage. The connecting handler also
always issued ConfirmedClose (half-close) regardless of the halfClose
setting once such an early-finished upstream was detected.

Modification:
- Hoist a connection eq null guard to the top of
  closeConnectionUpstreamFinished() so neither close branch dereferences a
  null connection; the close is deferred until the connection is
  established, mirroring closeConnectionDownstreamFinished().
- Make the connecting handler honor the half-close setting: send Close when
  half-close is disabled and ConfirmedClose when enabled.

Result:
Outbound connections with halfClose=false no longer throw a
NullPointerException when the upstream finishes before the connection is
established, and the early-finish close path respects the configured
half-close semantics.

Tests:
- stream-tests/testOnly org.apache.pekko.stream.io.TcpSpec (28 passed).
  Added a directional test that fails with the NPE before the fix.

References:
Fixes #3255
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NPE in TcpConnectionStage when halfClose=false and upstream finishes before connection established

1 participant