Files
natsdotnet/gaps/websocket.md
Joseph Doherty c30e67a69d Fix E2E test gaps and add comprehensive E2E + parity test suites
- Fix pull consumer fetch: send original stream subject in HMSG (not inbox)
  so NATS client distinguishes data messages from control messages
- Fix MaxAge expiry: add background timer in StreamManager for periodic pruning
- Fix JetStream wire format: Go-compatible anonymous objects with string enums,
  proper offset-based pagination for stream/consumer list APIs
- Add 42 E2E black-box tests (core messaging, auth, TLS, accounts, JetStream)
- Add ~1000 parity tests across all subsystems (gaps closure)
- Update gap inventory docs to reflect implementation status
2026-03-12 14:09:23 -04:00

18 KiB
Raw Blame History

WebSocket — Gap Analysis

This file tracks what has and hasn't been ported from Go to .NET for the WebSocket module. See stillmissing.md for the full LOC comparison across all modules.

LLM Instructions: How to Analyze This Category

Step 1: Read the Go Reference Files

Read each Go source file listed below. For every file:

  1. Extract all exported types (structs, interfaces, type aliases)
  2. Extract all exported methods on those types (receiver functions)
  3. Extract all exported standalone functions
  4. Note key constants, enums, and protocol states
  5. Note important unexported helpers that implement core logic (functions >20 lines)
  6. Pay attention to concurrency patterns (goroutines, mutexes, channels) — these map to different .NET patterns

Step 2: Read the .NET Implementation Files

Read all .cs files in the .NET directories listed below. For each Go symbol found in Step 1:

  1. Search for a matching type, method, or function in .NET
  2. If found, compare the behavior: does it handle the same edge cases? Same error paths?
  3. If partially implemented, note what's missing
  4. If not found, note it as MISSING

Step 3: Cross-Reference Tests

Compare Go test functions against .NET test methods:

  1. For each Go Test* function, check if a corresponding .NET [Fact] or [Theory] exists
  2. Note which test scenarios are covered and which are missing
  3. Check the parity DB (docs/test_parity.db) for existing mappings:
    sqlite3 docs/test_parity.db "SELECT go_test, dotnet_test, confidence FROM test_mappings tm JOIN go_tests gt ON tm.go_test_id=gt.rowid JOIN dotnet_tests dt ON tm.dotnet_test_id=dt.rowid WHERE gt.go_file LIKE '%PATTERN%'"
    

Step 4: Classify Each Item

Use these status values:

Status Meaning
PORTED Equivalent exists in .NET with matching behavior
PARTIAL .NET implementation exists but is incomplete (missing edge cases, error handling, or features)
MISSING No .NET equivalent found — needs to be ported
NOT_APPLICABLE Go-specific pattern that doesn't apply to .NET (build tags, platform-specific goroutine tricks, etc.)
DEFERRED Intentionally skipped for now (document why)

Step 5: Fill In the Gap Inventory

Add rows to the Gap Inventory table below. Group by Go source file. Include the Go file and line number so a porting LLM can jump directly to the reference implementation.

Key Porting Notes for WebSocket

  • WebSocket is at 99% LOC parity — likely near-complete.
  • Focus analysis on edge cases: compression negotiation, frame fragmentation, close handshake.
  • WebSocket connections wrap a standard NATS connection with WS framing.

Go Reference Files (Source)

  • golang/nats-server/server/websocket.go — WebSocket transport support (~1,550 lines). WS framing, upgrade handshake, compression, masking.

Go Reference Files (Tests)

  • golang/nats-server/server/websocket_test.go

.NET Implementation Files (Source)

  • src/NATS.Server/WebSocket/ (all files)

.NET Implementation Files (Tests)

  • tests/NATS.Server.Tests/WebSocket/

Gap Inventory

golang/nats-server/server/websocket.go

Types and Structs

Go Symbol Go File:Line Status .NET Equivalent Notes
wsOpCode (type) websocket.go:41 PORTED src/NATS.Server/WebSocket/WsConstants.cs:1 Int constants replace Go type alias; WsConstants.TextMessage, BinaryMessage, etc.
websocket (struct) websocket.go:108 PORTED src/NATS.Server/WebSocket/WsConnection.cs:8 Fields mapped: compress, maskread/maskwrite, browser, nocompfrag. Cookie fields moved to WsUpgradeResult. frames/fs buffer management is now in WsConnection.WriteAsync. compressor (reuse) is NOT pooled — recreated per call.
srvWebsocket (struct) websocket.go:126 PARTIAL src/NATS.Server/NatsServer.cs:538 _wsListener, _options.WebSocket cover port/host/tls; allowedOrigins managed via WsOriginChecker; explicit authOverride flag is now computed via WsAuthConfig.Apply(...). connectURLsMap ref-count URL set is still missing.
allowedOrigin (struct) websocket.go:145 PORTED src/NATS.Server/WebSocket/WsOriginChecker.cs:80 Private AllowedOrigin record struct with Scheme and Port.
wsUpgradeResult (struct) websocket.go:150 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:346 WsUpgradeResult readonly record struct with equivalent fields. kind maps to WsClientKind enum.
wsReadInfo (struct) websocket.go:156 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:10 All fields ported: remRemaining, fsFrameStart, ffFirstFrame, fcFrameCompressed, maskExpectMask, mkposMaskKeyPos, mkeyMaskKey, cbufsCompressedBuffers, coffCompressedOffset. Extra .NET fields added for control frame output.
WsDeflateParams (struct) websocket.go:885916 PORTED src/NATS.Server/WebSocket/WsCompression.cs:10 New .NET-specific struct capturing permessage-deflate negotiated parameters. No Go equivalent struct — Go stores compress bool only.

Package-Level Variables / Constants

Go Symbol Go File:Line Status .NET Equivalent Notes
wsOpCode constants websocket.go:4396 PORTED src/NATS.Server/WebSocket/WsConstants.cs:965 All opcode, bit, size, close-status, and header-string constants are present.
decompressorPool websocket.go:99 PARTIAL src/NATS.Server/WebSocket/WsCompression.cs:193 Go uses sync.Pool for flate.Reader reuse. .NET creates a new DeflateStream per decompression call — no pooling. Functional but slightly less efficient under high load.
compressLastBlock websocket.go:100 PORTED src/NATS.Server/WebSocket/WsConstants.cs:62 .NET uses 4-byte DecompressTrailer (sync marker only); Go uses 9-byte block. Both work correctly — difference is .NET DeflateStream does not need the final stored block.
wsGUID websocket.go:103 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:177 Inline string literal in ComputeAcceptKey.
wsTestRejectNoMasking websocket.go:106 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:14 Added test hook RejectNoMaskingForTest; when set, no-masking leaf upgrade requests are explicitly rejected

Methods on wsReadInfo

Go Symbol Go File:Line Status .NET Equivalent Notes
wsReadInfo.init() websocket.go:168 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:28 Constructor initializes FrameStart=true, FirstFrame=true (same as Go init()).
wsReadInfo.Read() websocket.go:353 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:100 Consumed data from CompressedBuffers is handled inside ReadFrames rather than via an io.Reader interface; functionally equivalent.
wsReadInfo.nextCBuf() websocket.go:376 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:100 Buffer cycling logic is inline within the Decompress method — no explicit nextCBuf helper needed since .NET uses list indexing.
wsReadInfo.ReadByte() websocket.go:393 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:100 Used by Go's flate.Resetter; .NET DeflateStream reads a MemoryStream directly — no ReadByte interface needed.
wsReadInfo.decompress() websocket.go:408 PORTED src/NATS.Server/WebSocket/WsCompression.cs:193 WsCompression.Decompress() — appends trailer, decompresses with DeflateStream, enforces maxPayload limit.
wsReadInfo.unmask() websocket.go:509 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:56 WsReadInfo.Unmask() — exact port including 8-byte bulk XOR optimization for buffers >= 16 bytes.
Go Symbol Go File:Line Status .NET Equivalent Notes
client.isWebsocket() websocket.go:197 PORTED src/NATS.Server/NatsClient.cs:107 NatsClient.IsWebSocket bool property.
client.wsRead() websocket.go:208 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:100 WsReadInfo.ReadFrames() — full state machine port with frame-type validation, extended-length decoding, mask read, control frame dispatch, and compression accumulation.
client.wsHandleControlFrame() websocket.go:445 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:246 WsReadInfo.HandleControlFrame() — ping→pong, close→echo-close, pong→no-op. Close UTF-8 body validation is MISSING (Go validates UTF-8 in close body and downgrades status to 1007 on failure; .NET does not).
client.wsEnqueueControlMessage() websocket.go:600 PORTED src/NATS.Server/WebSocket/WsConnection.cs:127 Control frames collected in PendingControlFrames and flushed via FlushControlFramesAsync. Go uses per-client write buffer queuing with flushSignal; .NET writes directly — functionally equivalent.
client.wsEnqueueControlMessageLocked() websocket.go:631 PORTED src/NATS.Server/WebSocket/WsConnection.cs:127 Combined with FlushControlFramesAsync. Lock semantics differ (Go client mu, .NET _writeLock).
client.wsEnqueueCloseMessage() websocket.go:668 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:152 WsFrameWriter.MapCloseStatus() covers the same ClosedState → status mapping. WsConnection.SendCloseAsync() performs the send. BadClientProtocolVersion, MaxAccountConnectionsExceeded, MaxConnectionsExceeded, MaxControlLineExceeded, MissingAccount, Revocation close-reason mappings are MISSING from .NET ClientClosedReason enum.
client.wsHandleProtocolError() websocket.go:700 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:135 Protocol errors throw InvalidOperationException in ReadFrames — caller (WsConnection.ReadAsync) treats any exception as a connection close. Direct close-frame send on protocol error is implicit via the close path.
client.wsCollapsePtoNB() websocket.go:1367 PARTIAL src/NATS.Server/WebSocket/WsConnection.cs:91 WsConnection.WriteFramedAsync() handles browser fragmentation and compression. However, Go's wsCollapsePtoNB is a low-level buffer-collapse routine that operates on net.Buffers (scatter/gather I/O) and reuses a persistent per-connection flate.Writer compressor. .NET recreates a DeflateStream per write and does not use scatter/gather — less efficient but functionally correct.

Standalone Functions

Go Symbol Go File:Line Status .NET Equivalent Notes
wsGet() websocket.go:178 PORTED src/NATS.Server/WebSocket/WsReadInfo.cs:299 WsReadInfo.WsGet() (private) — returns (byte[], newPos) tuple. Same buffer-then-reader fallback logic.
wsIsControlFrame() websocket.go:539 PORTED src/NATS.Server/WebSocket/WsConstants.cs:64 WsConstants.IsControlFrame(int opcode) — same >= CloseMessage check.
wsCreateFrameHeader() websocket.go:545 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:17 WsFrameWriter.CreateFrameHeader() — first=true, final=true wrapper.
wsFillFrameHeader() websocket.go:551 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:30 WsFrameWriter.FillFrameHeader() — exact port with 2/4/10-byte length encoding and optional masking.
wsMaskBuf() websocket.go:607 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:77 WsFrameWriter.MaskBuf() — simple XOR loop.
wsMaskBufs() websocket.go:614 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:86 WsFrameWriter.MaskBufs() — multi-buffer contiguous XOR.
wsCreateCloseMessage() websocket.go:711 PORTED src/NATS.Server/WebSocket/WsFrameWriter.cs:103 WsFrameWriter.CreateCloseMessage() — 2-byte status + body, body truncated with "..." at MaxControlPayloadSize-2. .NET additionally validates UTF-8 boundary when truncating.
wsUpgrade() (Server method) websocket.go:731 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:13 WsUpgrade.TryUpgradeAsync() — full RFC 6455 handshake: method, host, upgrade, connection, key, version checks; origin; compression negotiation; no-masking; browser detection; cookie extraction; X-Forwarded-For; custom headers; 101 response. Go uses http.Hijacker pattern; .NET reads raw TCP stream. MQTT Sec-Websocket-Protocol header in response is MISSING.
wsHeaderContains() websocket.go:872 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:315 WsUpgrade.HeaderContains() (private) — same comma-split, case-insensitive token matching.
wsPMCExtensionSupport() websocket.go:885 PORTED src/NATS.Server/WebSocket/WsCompression.cs:60 WsDeflateNegotiator.Negotiate() — parses Sec-WebSocket-Extensions, detects permessage-deflate, extracts server_no_context_takeover, client_no_context_takeover, and window bits.
wsReturnHTTPError() websocket.go:921 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:226 WsUpgrade.FailAsync() (private) — sends HTTP error response and returns WsUpgradeResult.Failed.
srvWebsocket.checkOrigin() websocket.go:933 PORTED src/NATS.Server/WebSocket/WsOriginChecker.cs:32 WsOriginChecker.CheckOrigin() — same-origin and allowed-list checks. Go checks r.TLS != nil for TLS detection; .NET uses isTls parameter passed at call site.
wsGetHostAndPort() websocket.go:985 PORTED src/NATS.Server/WebSocket/WsOriginChecker.cs:65 WsOriginChecker.GetHostAndPort() and ParseHostPort() — missing-port defaults to 80/443 by TLS flag.
wsAcceptKey() websocket.go:1004 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:175 WsUpgrade.ComputeAcceptKey() — SHA-1 of key + GUID, base64 encoded.
wsMakeChallengeKey() websocket.go:1011 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:192 Added MakeChallengeKey() generating a random base64-encoded 16-byte challenge nonce
validateWebsocketOptions() websocket.go:1020 PORTED src/NATS.Server/WebSocket/WebSocketOptionsValidator.cs:11 Added WebSocketOptionsValidator.Validate(NatsOptions) covering TLS cert/key requirement, allowed-origin URI parsing, NoAuthUser membership, username/token conflicts with users/nkeys, JwtCookie trusted-operator requirement, TLS pinned-cert validation, and reserved response-header protection. Startup now enforces this validation in NatsServer.StartAsync() before opening WS listener (src/NATS.Server/NatsServer.cs:631).
Server.wsSetOriginOptions() websocket.go:1083 PARTIAL src/NATS.Server/WebSocket/WsUpgrade.cs:49 Origin checking is constructed inline in TryUpgradeAsync from options.SameOrigin and options.AllowedOrigins. The Go method persists parsed origins in srvWebsocket.allowedOrigins map and supports hot-reload. .NET constructs a WsOriginChecker per request — no hot-reload support, but functionally equivalent for initial config.
Server.wsSetHeadersOptions() websocket.go:1111 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:141 Custom headers applied inline in TryUpgradeAsync from options.Headers.
Server.wsConfigAuth() websocket.go:1131 PORTED src/NATS.Server/WebSocket/WsAuthConfig.cs:5 Added explicit auth-override computation (Username/Token/NoAuthUser) and startup application via NatsServer.StartAsync() before WS listener initialization
Server.startWebsocketServer() websocket.go:1137 PORTED src/NATS.Server/NatsServer.cs:538 NatsServer.StartAsync() section at line 538 sets up the WS listener, logs, and launches RunWebSocketAcceptLoopAsync. Go uses http.Server + mux; .NET uses raw TcpListener/Socket.AcceptAsync. LEAF and MQTT routing at connection time is PARTIAL — LEAF path is wired (WsClientKind.Leaf) but MQTT is not handled in AcceptWebSocketClientAsync. lame-duck / ldmCh signaling is MISSING.
Server.wsGetTLSConfig() websocket.go:1264 PARTIAL src/NATS.Server/NatsServer.cs:807 TLS is applied once at accept time via TlsConnectionWrapper.NegotiateAsync. Go uses GetConfigForClient callback for hot-reload TLS config. .NET does not support hot TLS config reload for WS.
Server.createWSClient() websocket.go:1273 PORTED src/NATS.Server/NatsServer.cs:799 AcceptWebSocketClientAsync() — creates WsConnection, constructs NatsClient, wires IsWebSocket/WsInfo, registers client. Go also sends INFO immediately and sets auth timer; .NET's NatsClient.RunAsync() handles INFO send and auth timer.
isWSURL() websocket.go:1544 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:203 Added helper to detect absolute ws:// URLs via URI scheme parsing
isWSSURL() websocket.go:1548 PORTED src/NATS.Server/WebSocket/WsUpgrade.cs:215 Added helper to detect absolute wss:// URLs via URI scheme parsing

Keeping This File Updated

After porting work is completed:

  1. Update status: Change MISSING → PORTED or PARTIAL → PORTED for each item completed
  2. Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
  3. Re-count LOC: Update the LOC numbers in stillmissing.md:
    # Re-count .NET source LOC for this module
    find src/NATS.Server/WebSocket/ -name '*.cs' -type f -exec cat {} + | wc -l
    # Re-count .NET test LOC for this module
    find tests/NATS.Server.Tests/WebSocket/ -name '*.cs' -type f -exec cat {} + | wc -l
    
  4. Add a changelog entry below with date and summary of what was ported
  5. Update the parity DB if new test mappings were created:
    sqlite3 docs/test_parity.db "INSERT INTO test_mappings (go_test_id, dotnet_test_id, confidence, notes) VALUES (?, ?, 'manual', 'ported in YYYY-MM-DD session')"
    

Change Log

Date Change By
2026-02-26 Ported validateWebsocketOptions() parity by adding WebSocketOptionsValidator, wiring startup enforcement, and adding focused validator tests including TLS pinned cert validation. codex
2026-02-25 Ported Server.wsConfigAuth() parity by adding WsAuthConfig auth-override computation and applying it during WS startup; added WebSocketOptions.AuthOverride plus focused tests. codex
2026-02-25 File created with LLM analysis instructions auto
2026-02-25 Full gap inventory populated: 37 Go symbols classified (26 PORTED, 7 PARTIAL, 4 MISSING, 0 NOT_APPLICABLE, 0 DEFERRED) auto