WebSocketSession.close(int) hand-wrote a raw, always-unmasked 4-byte CLOSE frame, bypassing writeFrame's maskOutgoing handling that sendText/send/sendPong already go through correctly.
A client-mode session (maskOutgoing = true — WS-client usage, e.g. Pathway's UpstreamWebSocketConnector relaying a proxied client's close to a backend) therefore sent an RFC-6455-invalid unmasked frame.
This was latent until this same HTTP/2 branch's readFrame rewrite added the receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects the malformed frame with WebSocketProtocolException("client frame must be masked") before ever exposing it as a CLOSE, silently dropping the close instead of relaying it.
Reproduced end to end via Pathway's ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend, which was failing deterministically against master.
Fix
close(int) now builds its 2-byte payload and calls the same writeFrame path every other outgoing frame uses, so masking (or not) follows maskOutgoing automatically. Existing server-mode close_setsClosedAndWritesFrame is unchanged (byte-for-byte identical output).
Test plan
New unit test close_masksWhenActingAsClient (mirrors existing sendText coverage)
New round-trip regression readFrame_acceptsCloseFrameWrittenByAClientSession reproducing the actual bug
Full flash module suite: 697/697 green
Verified end-to-end against Pathway's ProxyWebSocketIntegrationTest (previously-failing test now passes)
## Summary
- `WebSocketSession.close(int)` hand-wrote a raw, always-unmasked 4-byte CLOSE frame, bypassing `writeFrame`'s `maskOutgoing` handling that `sendText`/`send`/`sendPong` already go through correctly.
- A client-mode session (`maskOutgoing = true` — WS-client usage, e.g. Pathway's `UpstreamWebSocketConnector` relaying a proxied client's close to a backend) therefore sent an RFC-6455-invalid unmasked frame.
- This was latent until this same HTTP/2 branch's `readFrame` rewrite added the receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects the malformed frame with `WebSocketProtocolException("client frame must be masked")` before ever exposing it as a CLOSE, silently dropping the close instead of relaying it.
- Reproduced end to end via Pathway's `ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend`, which was failing deterministically against `master`.
## Fix
`close(int)` now builds its 2-byte payload and calls the same `writeFrame` path every other outgoing frame uses, so masking (or not) follows `maskOutgoing` automatically. Existing server-mode `close_setsClosedAndWritesFrame` is unchanged (byte-for-byte identical output).
## Test plan
- [x] New unit test `close_masksWhenActingAsClient` (mirrors existing `sendText` coverage)
- [x] New round-trip regression `readFrame_acceptsCloseFrameWrittenByAClientSession` reproducing the actual bug
- [x] Full `flash` module suite: 697/697 green
- [x] Verified end-to-end against Pathway's `ProxyWebSocketIntegrationTest` (previously-failing test now passes)
WebSocketSession.close(int) hand-wrote a raw, always-unmasked 4-byte CLOSE
frame, bypassing writeFrame's maskOutgoing handling that sendText/send/
sendPong already go through correctly. A client-mode session (maskOutgoing
true — WS-client usage, e.g. Pathway's UpstreamWebSocketConnector relaying a
proxied client's close to a backend) therefore sent an RFC-6455-invalid
unmasked frame.
This was latent until this same HTTP/2 branch's readFrame rewrite added the
receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects
the malformed frame with WebSocketProtocolException("client frame must be
masked") before ever exposing it as a CLOSE, silently dropping the close
instead of relaying it — reproduced end to end via Pathway's
ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend.
close(int) now builds its 2-byte payload and calls the same writeFrame path
every other outgoing frame uses, so masking (or not) follows maskOutgoing
automatically. Existing server-mode close_setsClosedAndWritesFrame is
unchanged (byte-for-byte identical output — no mask bit, no key). Added
close_masksWhenActingAsClient (mirrors the existing sendText coverage) and a
round-trip regression, readFrame_acceptsCloseFrameWrittenByAClientSession,
that reproduces the actual bug: a client session's close() output fed
straight into a server session's readFrame().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa
Relism
merged commit 5954cac66c into master2026-08-14 23:02:13 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
WebSocketSession.close(int)hand-wrote a raw, always-unmasked 4-byte CLOSE frame, bypassingwriteFrame'smaskOutgoinghandling thatsendText/send/sendPongalready go through correctly.maskOutgoing = true— WS-client usage, e.g. Pathway'sUpstreamWebSocketConnectorrelaying a proxied client's close to a backend) therefore sent an RFC-6455-invalid unmasked frame.readFramerewrite added the receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects the malformed frame withWebSocketProtocolException("client frame must be masked")before ever exposing it as a CLOSE, silently dropping the close instead of relaying it.ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend, which was failing deterministically againstmaster.Fix
close(int)now builds its 2-byte payload and calls the samewriteFramepath every other outgoing frame uses, so masking (or not) followsmaskOutgoingautomatically. Existing server-modeclose_setsClosedAndWritesFrameis unchanged (byte-for-byte identical output).Test plan
close_masksWhenActingAsClient(mirrors existingsendTextcoverage)readFrame_acceptsCloseFrameWrittenByAClientSessionreproducing the actual bugflashmodule suite: 697/697 greenProxyWebSocketIntegrationTest(previously-failing test now passes)WebSocketSession.close(int) hand-wrote a raw, always-unmasked 4-byte CLOSE frame, bypassing writeFrame's maskOutgoing handling that sendText/send/ sendPong already go through correctly. A client-mode session (maskOutgoing true — WS-client usage, e.g. Pathway's UpstreamWebSocketConnector relaying a proxied client's close to a backend) therefore sent an RFC-6455-invalid unmasked frame. This was latent until this same HTTP/2 branch's readFrame rewrite added the receive-side masking check RFC 6455 §5.1 requires: a strict peer now rejects the malformed frame with WebSocketProtocolException("client frame must be masked") before ever exposing it as a CLOSE, silently dropping the close instead of relaying it — reproduced end to end via Pathway's ProxyWebSocketIntegrationTest.clientCloseIsForwardedToTheBackend. close(int) now builds its 2-byte payload and calls the same writeFrame path every other outgoing frame uses, so masking (or not) follows maskOutgoing automatically. Existing server-mode close_setsClosedAndWritesFrame is unchanged (byte-for-byte identical output — no mask bit, no key). Added close_masksWhenActingAsClient (mirrors the existing sendText coverage) and a round-trip regression, readFrame_acceptsCloseFrameWrittenByAClientSession, that reproduces the actual bug: a client session's close() output fed straight into a server session's readFrame(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LLwcyHUnbApCrY33gvgoa