- 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
18 KiB
18 KiB
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:
- Extract all exported types (structs, interfaces, type aliases)
- Extract all exported methods on those types (receiver functions)
- Extract all exported standalone functions
- Note key constants, enums, and protocol states
- Note important unexported helpers that implement core logic (functions >20 lines)
- 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:
- Search for a matching type, method, or function in .NET
- If found, compare the behavior: does it handle the same edge cases? Same error paths?
- If partially implemented, note what's missing
- If not found, note it as MISSING
Step 3: Cross-Reference Tests
Compare Go test functions against .NET test methods:
- For each Go
Test*function, check if a corresponding .NET[Fact]or[Theory]exists - Note which test scenarios are covered and which are missing
- 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: rem→Remaining, fs→FrameStart, ff→FirstFrame, fc→FrameCompressed, mask→ExpectMask, mkpos→MaskKeyPos, mkey→MaskKey, cbufs→CompressedBuffers, coff→CompressedOffset. Extra .NET fields added for control frame output. |
WsDeflateParams (struct) |
websocket.go:885–916 | 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:43–96 | PORTED | src/NATS.Server/WebSocket/WsConstants.cs:9–65 |
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. |
Methods on client (WebSocket-related)
| 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:
- Update status: Change
MISSING → PORTEDorPARTIAL → PORTEDfor each item completed - Add .NET path: Fill in the ".NET Equivalent" column with the actual file:line
- 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 - Add a changelog entry below with date and summary of what was ported
- 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 |