Skip to content

fix: resolve 7 quality issues (#100-#106) - #107

Merged
AlexStocks merged 8 commits into
masterfrom
fix/quality-issues-100-106
Aug 1, 2026
Merged

fix: resolve 7 quality issues (#100-#106)#107
AlexStocks merged 8 commits into
masterfrom
fix/quality-issues-100-106

Conversation

@AlexStocks

@AlexStocks AlexStocks commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

This PR fixes all 7 quality issues filed in the previous audit (#100#106) in a single change set.

Changes by issue

#100 — [security] Client TLS verification disabled

  • Removed InsecureSkipVerify: true from ClientTlsConfigBuilder.BuildTlsConfig() (tls.go) and from dialWSS() (client.go). The flag was disabling certificate verification entirely, making the configured RootCAs useless and exposing WSS/TLS clients to MITM attacks.

#101 — [crash] TLS builder (nil, nil) return → nil-pointer panic

  • ServerTlsConfigBuilder and ClientTlsConfigBuilder returned nil, err where err was nil (from a prior successful os.ReadFile) when cert parsing failed. Now return an explicit fmt.Errorf.
  • Hardened the call sites in dialTCP() and listenTCP() to treat a nil config/error from the builder as a real failure instead of falling through to a nil-conn dereference.

#102 — [data corruption] Compression send path broken

  • gettyTCPConn.Send routed [][]byte payloads via net.Buffers.WriteTo(t.conn), bypassing the compress writer. When compression was enabled the peer received a corrupt mix of compressed and raw frames. Now the [][]byte path goes through t.writer when compress != CompressNone (the fast net.Buffers writev path is kept for the no-compression case).
  • snappy.NewBufferedWriter was never flushed, so small packets sat in its internal buffer forever. Added a snappyWriteFlusher wrapper (mirroring the existing flate writeFlusher) that flushes on every Write. CloseConn updated to match the new wrapper type.
  • This is very likely the root cause of the long-standing 当compress_encoding设置成false,会出现i/o timeout的问题 #14.

#103 — [concurrency] WritePkg races gc(); per-call timeout leaks; CloseConn hard-assert

  • WritePkg/WriteBytes/WriteBytesArray accessed s.Connection under packetLock only, racing with gc() which nils it under s.lock. They now snapshot Connection under s.lock and nil-check before Send.
  • WritePkg's per-call SetWriteTimeout(timeout) permanently rewrote the connection write deadline. Now saves and restores the original timeout.
  • CloseConn did t.conn.(*tls.Conn).Close() (hard assert). Replaced with safe if/else if/else type assertions so a non-TLS, non-TCP conn no longer panics.

#104 — [logging] 4 printf verb/argument mismatches

  • session.go (missing bufLen arg for %d), client.go dialUDP (missing timeout arg — dropped the verb since UDP has no dial timeout), server.go accept & serveWSRequest (format string had 1 %s but 2 args — added the second %s). These are invisible to go vet because of the custom logger wrapper.

#105 — [lifecycle] Reset/client.wg/server Listener

  • session.Reset() replaced the whole struct (*s = session{...}) without holding s.lock, racing concurrent readers and copying mutexes. Now resets fields individually under s.lock.
  • client.wg was never Added, so Close()'s wg.Wait() returned immediately even while reConnect was still running. RunEventLoop now does wg.Add(1)/defer wg.Done() around reConnect.
  • server.Listener()/PacketConn() read streamListener/pktListener without a lock while stop() nils them. server.lock upgraded to sync.RWMutex; accessors take RLock, and stop() reads+nils the listeners under the lock (closes them outside the lock to avoid blocking).

#106 — [test/reconnect] TestTCPClient hangs forever

  • dialTCP/dialUDP/dialWS/dialWSS each had an unbounded for{} retry loop that only exited on success or IsClosed(). The maxReconnectAttempts guard lives in reConnect() above the dial call, so it was unreachable while the dial loop spun — WithReconnectAttempts was effectively a no-op and an unreachable target made the client (and TestTCPClient) hang forever. Each dial function now performs a single attempt and returns nil on failure; reConnect() owns the bounded back-off and attempt counter.
  • TestTCPClient listened on :0[::]:port (the unspecified IPv6 address, not a valid connect destination) and dialed that. Changed to 127.0.0.1:0.
  • Added go test -timeout guidance in the issue; recommend CI adopt -timeout 120s.

Verification

  • go build ./transport/ ./util/
  • go vet ./transport/ ./util/ ✅ (the pre-existing go vet noise from mutex copies in Reset is gone)
  • TestTCPClient: the hang is fixed — the test now terminates (previously hung until the 10-min go test timeout). In the sandbox where this was prepared, loopback TCP is blocked by the environment, so the connection itself can't be established here; on a normal host/CI with loopback available the test connects and passes. The key fix is that an unreachable target no longer spins forever.

Files changed

 transport/client.go      | 219 +++++++++++++++++++++++++----------------------
 transport/client_test.go |   6 +-
 transport/connection.go  |  60 +++++++++++--
 transport/server.go      |  49 +++++++----
 transport/session.go     |  78 +++++++++++++----
 transport/tls.go         |  18 ++--
 6 files changed, 285 insertions(+), 145 deletions(-)

Closes #100, closes #101, closes #102, closes #103, closes #104, closes #105, closes #106.

Summary by CodeRabbit

  • Bug Fixes
    • Improved connection establishment and bounded retry behavior across supported protocols.
    • Strengthened TLS validation, certificate handling, and secure certificate verification.
    • Resolved shutdown and lifecycle races affecting connections, sessions, callbacks, and listeners.
    • Improved compressed data delivery and processing of buffered data before connection closure.
    • Enhanced timeout handling, diagnostics, and resource cleanup.

    and dialWSS so configured RootCAs actually verify peer certs (MITM fix).
    return explicit errors instead of nil-deref panics.
    compression is enabled; wrap snappy writer so every Write is flushed
    (small packets no longer stuck in buffer). Likely fixes #14.
    concurrent gc() nil-ing Connection; save & restore write timeout so a
    per-call timeout does not permanently rewrite the conn deadline; replace
    CloseConn hard-assert of *tls.Conn with safe type assertions.
    server.go x2) invisible to go vet due to custom logger wrapper.
    copy); wire up client.wg so Close() waits for reConnect; synchronize
    server Listener()/PacketConn()/stop() via RWMutex.
    (single attempt per call, reConnect owns back-off) so WithReconnectAttempts
    is honored and unreachable targets no longer spin forever; fix TestTCPClient
    to dial 127.0.0.1 instead of the unspecified [::] listener address.
@AlexStocks
AlexStocks force-pushed the fix/quality-issues-100-106 branch from a904c27 to 3d54f9c Compare July 31, 2026 07:23
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The transport package bounds protocol dial attempts, validates TLS configurations and certificates, flushes compressed TCP writes, synchronizes session and server lifecycle state, and waits for reconnect work during close.

Changes

Transport reliability

Layer / File(s) Summary
Bounded client dialing and TLS validation
transport/client.go, transport/tls.go, transport/client_test.go, transport/tls_test.go
Each protocol dial method performs one attempt before reconnect handling. Reconnect attempts are bounded and cancellable. TLS builders return explicit errors, require TLS 1.2, and verify certificates with configured root CAs.
Flushed compressed TCP writes
transport/connection.go, transport/connection_test.go
Snappy writes are serialized and flushed after each write. Compressed byte-array sends use the compression writer. Connection close handles wrapped and unwrapped connection types safely.
Synchronized session lifecycle and writes
transport/session.go, transport/session_test.go, transport/session_callback_test.go
Session reset waits for active lifecycle work. Writes use synchronized connection snapshots, restore per-call timeouts, and return ErrSessionClosed when needed. EOF handling preserves data returned with io.EOF.
Synchronized server shutdown and TLS setup
transport/server.go, transport/server_test.go
Server listener access uses read/write locks. Shutdown closes listener snapshots after unlocking and preserves published listener fields. TLS setup propagates builder errors and rejects nil configurations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RunEventLoop
  participant reConnect
  participant dialTCP
  participant TCPServer
  RunEventLoop->>reConnect: start bounded reconnect loop
  reConnect->>dialTCP: request one dial attempt
  dialTCP->>TCPServer: dial TCP
  TCPServer-->>dialTCP: connection or failure
  dialTCP-->>reConnect: return session or failure
  reConnect-->>RunEventLoop: continue, back off, or stop
Loading

Possibly related PRs

  • AlexStocks/getty#84: Both PRs modify reconnect attempt and backoff handling in transport/client.go.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the seven linked quality fixes addressed by the pull request.
Linked Issues check ✅ Passed The implementation and tests address the coding objectives in issues #100#106, including TLS, compression, concurrency, logging, lifecycle, and reconnect bounds.
Out of Scope Changes check ✅ Passed The production and test changes are directly related to the requirements in issues #100#106.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/quality-issues-100-106

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
transport/tls.go (1)

118-124: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set MinVersion on the new client TLS configurations. Both configurations enable real certificate verification but leave the minimum protocol version at the Go default, so legacy TLS versions remain acceptable. Pin MinVersion in each configuration.

  • transport/tls.go#L118-L124: add MinVersion: tls.VersionTLS12 (or TLS 1.3) to the returned tls.Config.
  • transport/client.go#L302-L305: add the same MinVersion field to the tls.Config used by dialWSS.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/tls.go` around lines 118 - 124, Pin the minimum TLS protocol
version to TLS 1.2 or newer in both configurations: add MinVersion to the
returned tls.Config in transport/tls.go (lines 118-124) and to the tls.Config
used by dialWSS in transport/client.go (lines 302-305), preserving their
existing certificate verification settings.

Source: Linters/SAST tools

transport/connection.go (1)

249-260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Return n when Flush fails.

Write succeeded before the Flush call, so the byte count is n, not 0. Callers ignore the count on error today, so this is cosmetic consistency with writeFlusher.

♻️ Proposed change
 	if err := s.writer.Flush(); err != nil {
-		return 0, perrors.WithStack(err)
+		return n, perrors.WithStack(err)
 	}

Also applies to: 280-282

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/connection.go` around lines 249 - 260, Update
snappyWriteFlusher.Write so a Flush failure returns the successful byte count n
together with the error instead of returning 0. Apply the same correction to the
corresponding writeFlusher implementation identified by the additional location,
while preserving existing error wrapping and write behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transport/client.go`:
- Around line 442-447: Make the backoff wait in reConnect cancellable by
selecting between the reconnect timer and the client’s shutdown signal or
cancellation channel used by stop(). Ensure stop() causes the wait to return
immediately, while preserving the existing retry timing when the client remains
open.

In `@transport/connection.go`:
- Around line 384-389: Add a Close method to snappyWriteFlusher that acquires
s.lock before closing the underlying snappy writer, then update the connection
close path to call writer.Close() instead of accessing writer.writer directly.

In `@transport/server.go`:
- Around line 210-224: The writes to s.streamListener in listenTCP() and
s.pktListener in listenUDP() are not protected by s.lock, while reads of these
fields in methods like Listener(), PacketConn(), and stop() hold the lock. Wrap
each assignment to s.streamListener and s.pktListener with s.lock.Lock() before
the assignment and s.lock.Unlock() after the assignment to prevent data races
between concurrent reads and writes.
- Around line 163-176: The stop() method nils s.streamListener and s.pktListener
after releasing the lock, but the accept() method reads s.streamListener without
holding the lock, creating a race where accept() can panic on a nil interface
method call. Either remove the nil assignments in stop() and keep the listeners
assigned while only closing them, or update accept() to read the listener
through a locked accessor that safely checks for nil before calling Accept() and
returns an appropriate closed-server error when nil.

In `@transport/session.go`:
- Around line 442-452: Serialize the temporary write-timeout lifecycle in
WritePkg: protect the origWriteTimeout capture, gc.SetWriteTimeout, conn.Send,
and restoration with a dedicated mutex so concurrent calls cannot interleave
values. Keep the existing no-timeout path behavior unchanged and ensure the
timeout is restored after sending.
- Around line 739-741: In the session.go file around Line 739-741, revert the
exit assignment from false back to true to avoid the unintended extra conn.recv
call on EOF, and fix the indentation of both the exit statement and the
log.Infof call so they are properly nested inside the if bufLen != 0 block.
After making these changes, run gofmt on the file to ensure consistent
formatting.

---

Nitpick comments:
In `@transport/connection.go`:
- Around line 249-260: Update snappyWriteFlusher.Write so a Flush failure
returns the successful byte count n together with the error instead of returning
0. Apply the same correction to the corresponding writeFlusher implementation
identified by the additional location, while preserving existing error wrapping
and write behavior.

In `@transport/tls.go`:
- Around line 118-124: Pin the minimum TLS protocol version to TLS 1.2 or newer
in both configurations: add MinVersion to the returned tls.Config in
transport/tls.go (lines 118-124) and to the tls.Config used by dialWSS in
transport/client.go (lines 302-305), preserving their existing certificate
verification settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2742695c-1f68-49f9-bb6c-988088b7ffc1

📥 Commits

Reviewing files that changed from the base of the PR and between 33d84ce and 3d54f9c.

📒 Files selected for processing (6)
  • transport/client.go
  • transport/client_test.go
  • transport/connection.go
  • transport/server.go
  • transport/session.go
  • transport/tls.go

Comment thread transport/client.go Outdated
Comment thread transport/connection.go
Comment thread transport/server.go Outdated
Comment thread transport/server.go Outdated
Comment thread transport/session.go Outdated
Comment thread transport/session.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
transport/server.go (1)

277-285: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

accept() still reads s.streamListener unguarded; can panic after stop().

stop() nils s.streamListener under s.lock (Line 166). accept() reads s.streamListener directly at Line 278, with no lock and no nil check, and calls .Accept() on it. If stop() runs concurrently with accept(), this call executes a method on a nil interface value, which panics.

Use the existing locked Listener() accessor (Lines 528-533) instead of reading the field directly, and return a closed-server error when it is nil. See the consolidated comment for the combined fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/server.go` around lines 277 - 285, Update server.accept to obtain
the listener through the existing locked Listener() accessor rather than reading
s.streamListener directly. If Listener() returns nil after stop(), return the
established closed-server error before calling Accept, while preserving the
existing accept and self-connection handling.
♻️ Duplicate comments (2)
transport/client.go (1)

438-449: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Close() blocks for the full backoff interval.

RunEventLoop now registers reConnect() with c.wg (Line 446), so Close()'s c.wg.Wait() (Line 515) genuinely waits for it. But reConnect() only checks c.IsClosed() at the top of its loop (Line 467) and then blocks on <-gxtime.After(maxReconnectInterval) (Line 480), which can be up to maxBackOffTimes * reconnectInterval. Close() therefore waits for the current sleep to finish before returning.

Make the backoff wait cancellable so stop() releases it immediately.

♻️ Proposed fix in `reConnect`
 		maxReconnectInterval = int64(math.Min(float64(reconnectAttempts), float64(maxBackOffTimes))) * int64(reconnectInterval)
-		<-gxtime.After(time.Duration(maxReconnectInterval))
+		select {
+		case <-c.done:
+			return
+		case <-gxtime.After(time.Duration(maxReconnectInterval)):
+		}
 	}

Also applies to: 452-482, 513-516

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/client.go` around lines 438 - 449, Make the backoff wait in
reConnect cancellable so Close does not block for the full reconnect interval.
Replace the non-cancellable gxtime.After wait with a select or equivalent that
returns immediately when the client’s stop signal is triggered, while preserving
the existing timeout-based retry behavior. Ensure stop() closes or signals the
same cancellation mechanism used by reConnect before c.wg.Wait() runs.
transport/server.go (1)

198-234: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

TLS/error handling fix looks correct; the unguarded listener write persists.

Lines 210-224 correctly propagate BuildTlsConfig errors and reject a nil config, matching the #101 objective.

Line 230 still writes s.streamListener = streamListener without holding s.lock, while Listener(), PacketConn(), and stop() all access the same field under s.lock. This is a data race on an interface-typed field. See the consolidated comment for the combined fix with accept().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/server.go` around lines 198 - 234, Protect the listener assignment
in listenTCP by acquiring s.lock before writing s.streamListener, and release it
after the assignment. Keep the existing address update behavior intact while
ensuring access to the shared interface field is synchronized consistently with
Listener(), PacketConn(), and stop().
🧹 Nitpick comments (2)
transport/client.go (1)

300-305: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set an explicit MinVersion on the WSS tls.Config.

This tls.Config{} literal does not set MinVersion, so the effective minimum for a Go TLS client defaults to TLS 1.2. Set MinVersion: tls.VersionTLS13 (or at least tls.VersionTLS12 explicitly) to make the intended minimum version explicit and avoid silently trusting the language default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/client.go` around lines 300 - 305, Update the WSS TLS configuration
literal in the client connection setup to set an explicit MinVersion, using
tls.VersionTLS13 or at least tls.VersionTLS12. Preserve the existing certificate
verification behavior and other TLS configuration semantics.

Source: Linters/SAST tools

transport/tls.go (1)

118-124: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set an explicit MinVersion on the client tls.Config.

This config removes InsecureSkipVerify, correctly restoring certificate verification (#100). It does not set MinVersion, so the effective minimum defaults to whatever the Go runtime uses for TLS clients. Set MinVersion: tls.VersionTLS13 (or at least tls.VersionTLS12 explicitly) to make the intended minimum version explicit rather than relying on the language default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/tls.go` around lines 118 - 124, Update the tls.Config construction
in the client TLS setup to set an explicit MinVersion, using tls.VersionTLS13 or
the required tls.VersionTLS12 minimum. Preserve the existing RootCAs,
Certificates, and certificate-verification behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transport/client.go`:
- Around line 328-341: Update the RootCAs setup in the certificate-handling flow
to assign config.RootCAs only when certPool received at least one root from
config.Certificates. Leave config.RootCAs unset when no client certificate is
configured, preserving the system trust store fallback used by dialWSS.
- Around line 329-338: Replace the panic in dialWSS’s certificate parsing loop
with the established failure path: log the parsing error and return nil so
reConnect() can retry. Preserve the existing certificate-pool construction for
valid certificates and match the non-panicking behavior of dialTCP, dialUDP, and
dialWS.

---

Outside diff comments:
In `@transport/server.go`:
- Around line 277-285: Update server.accept to obtain the listener through the
existing locked Listener() accessor rather than reading s.streamListener
directly. If Listener() returns nil after stop(), return the established
closed-server error before calling Accept, while preserving the existing accept
and self-connection handling.

---

Duplicate comments:
In `@transport/client.go`:
- Around line 438-449: Make the backoff wait in reConnect cancellable so Close
does not block for the full reconnect interval. Replace the non-cancellable
gxtime.After wait with a select or equivalent that returns immediately when the
client’s stop signal is triggered, while preserving the existing timeout-based
retry behavior. Ensure stop() closes or signals the same cancellation mechanism
used by reConnect before c.wg.Wait() runs.

In `@transport/server.go`:
- Around line 198-234: Protect the listener assignment in listenTCP by acquiring
s.lock before writing s.streamListener, and release it after the assignment.
Keep the existing address update behavior intact while ensuring access to the
shared interface field is synchronized consistently with Listener(),
PacketConn(), and stop().

---

Nitpick comments:
In `@transport/client.go`:
- Around line 300-305: Update the WSS TLS configuration literal in the client
connection setup to set an explicit MinVersion, using tls.VersionTLS13 or at
least tls.VersionTLS12. Preserve the existing certificate verification behavior
and other TLS configuration semantics.

In `@transport/tls.go`:
- Around line 118-124: Update the tls.Config construction in the client TLS
setup to set an explicit MinVersion, using tls.VersionTLS13 or the required
tls.VersionTLS12 minimum. Preserve the existing RootCAs, Certificates, and
certificate-verification behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 05973821-a712-4d48-b9c9-5aa14992df88

📥 Commits

Reviewing files that changed from the base of the PR and between 33d84ce and 3d54f9c.

📒 Files selected for processing (6)
  • transport/client.go
  • transport/client_test.go
  • transport/connection.go
  • transport/server.go
  • transport/session.go
  • transport/tls.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • transport/connection.go
  • transport/session.go

Comment thread transport/client.go Outdated
Comment thread transport/client.go Outdated
Comment thread transport/client.go
Comment thread transport/client.go Outdated
Comment thread transport/session.go
Comment thread transport/server.go Outdated
AlexStocks and others added 2 commits July 31, 2026 16:47
Serialize per-write timeout mutation, track every reconnect loop during client shutdown, and protect callback reset with its owning mutex. Add deterministic regression coverage for retry bounds, cancellation, lifecycle tracking, timeout restoration, and callback synchronization.

Constraint: Preserve existing public APIs and keep the change within transport lifecycle paths.

Confidence: High; targeted regressions pass repeatedly and static and build checks pass.

Scope-risk: Reconnect scheduling, client shutdown, callback reset, and timed session writes.

Tested: Focused regressions x10; related non-network transport tests; go vet ./transport ./util; go build ./transport ./util; gofmt and imports-formatter.

Not-tested: Full Windows WSS suite and Linux race execution; the pushed PR CI will provide Ubuntu coverage.

Co-authored-by: OmX <omx@oh-my-codex.dev>
Synchronize session reset with package, callback, heartbeat, and connection cleanup work so pooled sessions cannot be cleared while old-generation activity is still using them. Preserve published server listeners across shutdown, serialize snappy close with writes, process final EOF bytes once, and build WSS trust roots without panics.

Constraint: Preserve existing reconnect behavior and use system trust roots when no custom WSS CA is configured.

Confidence: High; deterministic regression tests and repeated WSL race runs cover the changed paths.

Scope-risk: Medium; session lifecycle and TLS trust construction affect core teardown and dial paths.

Tested: WSL Go 1.25 go test -race ./transport ./util; WSL go test ./...; targeted race tests at count 10; TestUDPClient at count 3; go vet ./transport ./util; go build ./transport ./util; golangci-lint run ./... --timeout=10m; gofmt/import formatting; git diff --check.

Not-tested: Windows full transport loopback suite remains blocked by the previously reproduced local loopback failure; GitHub CI will validate the pushed Head.

Co-authored-by: OmX <omx@oh-my-codex.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
transport/server.go (1)

163-176: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard listener publication during shutdown.

If Close() runs while listenTCP() or listenUDP() is binding, stop() can snapshot nil listener fields and finish. The listen method can then publish a newly bound listener after shutdown starts. stop() will not close that listener again, so the socket can leak.

Check s.done before binding and again under s.lock immediately before publishing. If shutdown has started, close the local listener and return through the normal closed-server path. Add a regression test for concurrent Close() and listener setup.

Also applies to: 228-232, 260-264

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/server.go` around lines 163 - 176, Guard listener setup in
listenTCP and listenUDP against shutdown by checking s.done before binding and
again while holding s.lock immediately before publishing the newly bound
listener. If shutdown has started, close the local listener and return through
the existing closed-server path instead of assigning it. Add a regression test
covering concurrent Close() with listener setup and ensuring the bound listener
is closed.
🧹 Nitpick comments (5)
transport/tls_test.go (1)

42-52: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add assertions for the #100 and #101 fixes in ClientTlsConfigBuilder.

The test asserts only MinVersion. Two other fixes in ClientTlsConfigBuilder.BuildTlsConfig have no coverage in this file:

  • #100 removed InsecureSkipVerify: true and relies on RootCAs. A regression that restores InsecureSkipVerify still passes this test.
  • #101 returns an explicit error when AppendCertsFromPEM fails. That path previously returned (nil, nil) and caused a nil-pointer dereference in dialTCP and dialWSS.

The fixtures are already in place, so both assertions are cheap to add.

💚 Proposed additional assertions
 	if config.MinVersion != tls.VersionTLS12 {
 		t.Fatalf("MinVersion = %d, want TLS 1.2 (%d)", config.MinVersion, tls.VersionTLS12)
 	}
+	if config.InsecureSkipVerify {
+		t.Fatal("InsecureSkipVerify is true; certificate verification must stay enabled")
+	}
+	if config.RootCAs == nil {
+		t.Fatal("RootCAs is nil; the configured trust collection was not loaded")
+	}
+	if len(config.Certificates) != 1 {
+		t.Fatalf("Certificates contains %d entries, want 1", len(config.Certificates))
+	}
 }
+
+func TestClientTLSConfigBuilderRejectsInvalidTrustCollection(t *testing.T) {
+	tempDir := t.TempDir()
+	certPath := filepath.Join(tempDir, "client.crt")
+	keyPath := filepath.Join(tempDir, "client.key")
+	caPath := filepath.Join(tempDir, "ca.crt")
+	for path, data := range map[string][]byte{
+		certPath: WssServerCRT,
+		keyPath:  WssServerKEY,
+		caPath:   []byte("not a certificate"),
+	} {
+		if err := os.WriteFile(path, data, 0o600); err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	config, err := (&ClientTlsConfigBuilder{
+		ClientKeyCertChainPath:        certPath,
+		ClientPrivateKeyPath:          keyPath,
+		ClientTrustCertCollectionPath: caPath,
+	}).BuildTlsConfig()
+	if err == nil {
+		t.Fatal("invalid trust collection returned nil error")
+	}
+	if config != nil {
+		t.Fatal("config must be nil when BuildTlsConfig fails")
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/tls_test.go` around lines 42 - 52, Extend the TLS configuration
test around ClientTlsConfigBuilder.BuildTlsConfig to assert that
InsecureSkipVerify remains disabled and RootCAs is populated, covering fix `#100`.
Add a case using the existing invalid certificate fixture to verify
BuildTlsConfig returns a non-nil error when AppendCertsFromPEM fails, covering
fix `#101`.
transport/client_test.go (1)

131-165: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

TestSessionReconnectIsTrackedByClose does not assert the ordering it names.

The test measures how long ss.stop() takes to return. It never records whether clt.Close() returned before the session-triggered reconnect finished. clt.Close() at Line 154 runs on the test goroutine and is not timed.

If Close returned early while the reconnect goroutine kept running, sessionStopDone would still close within 100ms and the test would pass. The failure message at Line 163 therefore describes a condition the test cannot observe.

Record the reconnect completion and the Close return, then compare them.

♻️ Proposed restructuring to assert the ordering explicitly
 	sessionStopDone := make(chan struct{})
 	go func() {
 		ss.stop()
 		close(sessionStopDone)
 	}()
 	select {
 	case <-builder.entered:
 	case <-time.After(time.Second):
 		t.Fatal("session-triggered reconnect did not start")
 	}
 
-	clt.Close()
-	select {
-	case <-sessionStopDone:
-	case <-time.After(100 * time.Millisecond):
-		select {
-		case <-sessionStopDone:
-		case <-time.After(3 * time.Second):
-			t.Fatal("session stop did not return after reconnect backoff elapsed")
-		}
-		t.Fatal("Close returned while the session-triggered reconnect was still running")
-	}
+	closeReturned := make(chan struct{})
+	go func() {
+		clt.Close()
+		close(closeReturned)
+	}()
+
+	// Close must not return before the reconnect goroutine finishes.
+	select {
+	case <-closeReturned:
+		select {
+		case <-sessionStopDone:
+		default:
+			t.Fatal("Close returned while the session-triggered reconnect was still running")
+		}
+	case <-time.After(3 * time.Second):
+		t.Fatal("Close did not return; the reconnect backoff was not cancelled")
+	}
+	select {
+	case <-sessionStopDone:
+	case <-time.After(time.Second):
+		t.Fatal("session stop did not return")
+	}

Note that this restructuring is only valid if session.stop registers its reconnect through client.runReconnect. That is the same contract flagged on transport/client.go Lines 423-445.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/client_test.go` around lines 131 - 165, Update
TestSessionReconnectIsTrackedByClose to explicitly record both reconnect
completion and clt.Close return, running Close asynchronously if needed, then
assert Close does not complete before the session-triggered reconnect finishes.
Preserve the existing timeout coverage and use the client’s reconnect-tracking
contract through runReconnect rather than inferring ordering from ss.stop()
completion.
transport/connection_test.go (1)

89-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use sync.Once for the release-channel cleanup.

The select with default closes release only when a receive is not ready. This works here because release never carries a value and is only closed. It is not a general "close once" primitive: a pending value would also satisfy the receive case and skip the close.

sync.Once states the intent directly and does not depend on that property.

♻️ Proposed cleanup
 	underlying := &blockingSnappyWriter{
 		entered: make(chan struct{}, 1),
 		release: make(chan struct{}),
 	}
-	defer func() {
-		select {
-		case <-underlying.release:
-		default:
-			close(underlying.release)
-		}
-	}()
+	var releaseOnce sync.Once
+	release := func() { releaseOnce.Do(func() { close(underlying.release) }) }
+	defer release()

Then replace Line 121:

-	close(underlying.release)
+	release()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/connection_test.go` around lines 89 - 95, Replace the select-based
cleanup around underlying.release with a sync.Once-based close-once mechanism,
defining and invoking the once guard in the relevant test setup and cleanup
paths. Ensure release is closed exactly once without relying on channel receive
behavior, and update the referenced cleanup at the later release call
accordingly.
transport/server.go (1)

221-221: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the noctx lint error at transport/server.go:221.

net.Listen is flagged because it has no cancellation context, while net.ListenConfig.Listen provides a context-aware alternative. Use net.ListenConfig.Listen with a shutdown/scoped context for TCP startup, or add a scoped exclusion only if this call cannot support cancellation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/server.go` at line 221, Replace the net.Listen call in the TCP
startup path with net.ListenConfig.Listen using the server’s shutdown or
appropriately scoped context, while preserving the existing address and
error-handling behavior. Only add a narrowly scoped noctx exclusion if
cancellation cannot be supported by the surrounding transport startup flow.

Source: Linters/SAST tools

transport/session_test.go (1)

129-154: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The read-loop release depends on the number of SetReadDeadline calls.

resetBarrierNetConn.SetReadDeadline releases the blocked Read only on the second call. The first call comes from gettyTCPConn.recv, and the second from stop(). If the connection layer changes how often it sets the read deadline, the test either releases early or hangs until the 1s timeouts fire.

Release the barrier on a signal that states the intent, for example a Close() call on the double or a dedicated channel that ss.Close() triggers.

Also applies to: 213-256

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/session_test.go` around lines 129 - 154, Update resetBarrierNetConn
so Read is released by an explicit close or dedicated synchronization signal
rather than counting SetReadDeadline calls. Make Close trigger that signal, and
adjust the related test setup around ss.Close so the blocked Read is
deterministically released while preserving the barrier’s intended ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@transport/server.go`:
- Around line 163-176: Guard listener setup in listenTCP and listenUDP against
shutdown by checking s.done before binding and again while holding s.lock
immediately before publishing the newly bound listener. If shutdown has started,
close the local listener and return through the existing closed-server path
instead of assigning it. Add a regression test covering concurrent Close() with
listener setup and ensuring the bound listener is closed.

---

Nitpick comments:
In `@transport/client_test.go`:
- Around line 131-165: Update TestSessionReconnectIsTrackedByClose to explicitly
record both reconnect completion and clt.Close return, running Close
asynchronously if needed, then assert Close does not complete before the
session-triggered reconnect finishes. Preserve the existing timeout coverage and
use the client’s reconnect-tracking contract through runReconnect rather than
inferring ordering from ss.stop() completion.

In `@transport/connection_test.go`:
- Around line 89-95: Replace the select-based cleanup around underlying.release
with a sync.Once-based close-once mechanism, defining and invoking the once
guard in the relevant test setup and cleanup paths. Ensure release is closed
exactly once without relying on channel receive behavior, and update the
referenced cleanup at the later release call accordingly.

In `@transport/server.go`:
- Line 221: Replace the net.Listen call in the TCP startup path with
net.ListenConfig.Listen using the server’s shutdown or appropriately scoped
context, while preserving the existing address and error-handling behavior. Only
add a narrowly scoped noctx exclusion if cancellation cannot be supported by the
surrounding transport startup flow.

In `@transport/session_test.go`:
- Around line 129-154: Update resetBarrierNetConn so Read is released by an
explicit close or dedicated synchronization signal rather than counting
SetReadDeadline calls. Make Close trigger that signal, and adjust the related
test setup around ss.Close so the blocked Read is deterministically released
while preserving the barrier’s intended ordering.

In `@transport/tls_test.go`:
- Around line 42-52: Extend the TLS configuration test around
ClientTlsConfigBuilder.BuildTlsConfig to assert that InsecureSkipVerify remains
disabled and RootCAs is populated, covering fix `#100`. Add a case using the
existing invalid certificate fixture to verify BuildTlsConfig returns a non-nil
error when AppendCertsFromPEM fails, covering fix `#101`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e11c039d-d214-4698-8f3c-f2a63579be01

📥 Commits

Reviewing files that changed from the base of the PR and between 3d54f9c and a299dc0.

📒 Files selected for processing (11)
  • transport/client.go
  • transport/client_test.go
  • transport/connection.go
  • transport/connection_test.go
  • transport/server.go
  • transport/server_test.go
  • transport/session.go
  • transport/session_callback_test.go
  • transport/session_test.go
  • transport/tls.go
  • transport/tls_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • transport/tls.go
  • transport/connection.go

Make listener publication and server shutdown share one synchronization boundary so a listener created concurrently with Close cannot escape after the server is already closed. Harden the related concurrency and TLS regression tests to assert lifecycle ordering and invalid trust input explicitly.

Constraint: Preserve the existing listener accessor behavior after a listener has been published and avoid widening public server lifecycle semantics.

Confidence: High; deterministic TCP and UDP close-versus-publish tests pass together with WSL race, repository tests, vet, and build checks.

Scope-risk: Limited to transport server shutdown synchronization and review-requested regression test strengthening.

Tested: WSL Go 1.25 targeted go test -race; go test ./...; go vet ./transport ./util; go build ./transport ./util; git diff --cached --check.

Not-tested: GitHub CI and CodeRabbit on this new commit are pending.

Co-authored-by: OmX <omx@oh-my-codex.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
transport/tls_test.go (1)

53-61: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert certificate contents, not only field presence.

config.RootCAs != nil only proves that a pool was allocated. len(config.Certificates) == 1 only proves the slice length. A regression that loads the wrong certificate or leaves the pool empty can pass this test. Assert that WssClientCRT is present in RootCAs and that WssServerCRT is loaded, or perform an in-memory handshake with trusted and untrusted certificates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transport/tls_test.go` around lines 53 - 61, Strengthen the TLS assertions in
the test around config.InsecureSkipVerify by verifying certificate contents, not
just allocation and counts. Confirm WssClientCRT is present in config.RootCAs
and that config.Certificates contains WssServerCRT, using the existing
certificate parsing or comparison helpers where available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@transport/tls_test.go`:
- Around line 53-61: Strengthen the TLS assertions in the test around
config.InsecureSkipVerify by verifying certificate contents, not just allocation
and counts. Confirm WssClientCRT is present in config.RootCAs and that
config.Certificates contains WssServerCRT, using the existing certificate
parsing or comparison helpers where available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2a5b2f3-bb19-4b6a-b910-5c8dae9658c5

📥 Commits

Reviewing files that changed from the base of the PR and between a299dc0 and 26d44cd.

📒 Files selected for processing (6)
  • transport/client_test.go
  • transport/connection_test.go
  • transport/server.go
  • transport/server_test.go
  • transport/session_test.go
  • transport/tls_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • transport/connection_test.go
  • transport/session_test.go
  • transport/client_test.go

Strengthen the client TLS builder regression test so it compares the configured trust pool and certificate chain against the requested PEM fixtures, preventing allocation-only assertions from masking wrong or empty certificate data.

Constraint: Keep production TLS behavior unchanged and limit the change to review-requested test evidence.

Confidence: High; the certificate-content assertions pass repeatedly with the project Go 1.25 toolchain.

Scope-risk: Test-only change in transport/tls_test.go.

Tested: WSL Go 1.25 targeted TLS builder tests with count=10; gofmt -d; git diff --check.

Not-tested: GitHub CI and CodeRabbit on this new commit are pending.

Co-authored-by: OmX <omx@oh-my-codex.dev>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@transport/tls_test.go`:
- Around line 65-81: Update the TLS test fixtures used by the assertions around
config.RootCAs and config.Certificates in the transport test setup so
WssServerCRT and WssClientCRT contain distinct PEM certificates. Preserve the
existing certificate and trust-store assertions, ensuring they can detect
swapped client-certificate and trust-collection inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74badef0-bd7b-4e28-8303-db921bb9a17a

📥 Commits

Reviewing files that changed from the base of the PR and between 26d44cd and d864afc.

📒 Files selected for processing (1)
  • transport/tls_test.go

Comment thread transport/tls_test.go
Replace the duplicated trust certificate in the TLS builder regression test with a separate self-signed root and assert that its DER differs from the client certificate. This makes the content checks fail if certificate and trust inputs are swapped.

Constraint: Leave shared WSS integration fixtures and production TLS behavior unchanged.

Confidence: High; repository fixtures were fingerprinted as identical and the revised assertions pass repeatedly with distinct certificate data.

Scope-risk: Test-only fixture and assertions in transport/tls_test.go.

Tested: WSL Go 1.25 targeted TLS builder tests with count=10; gofmt -d; git diff --check.

Not-tested: GitHub CI and CodeRabbit on this new commit are pending.

Co-authored-by: OmX <omx@oh-my-codex.dev>
@AlexStocks

Copy link
Copy Markdown
Owner Author

补充收口 CodeRabbit review 4832624383 中无法创建行内线程的 listener publication finding:

  • 26d44cd 让 listener 发布与 Close 使用同一把锁检查 done;若关闭先发生,迟到的 TCP/UDP listener 会立即关闭并返回 errServerClosed,不会在 shutdown 后泄漏或继续对外可见。
  • TestServerClosePreventsLateListenerPublication 对 TCP、UDP 和已关闭 server 三条路径做确定性并发验证;相关定向测试在 WSL Go 1.25 -race 下通过,仓库级 go test ./...go vet ./transport ./utilgo build ./transport ./util 通过。
  • 最终 Head f654f82 的 GitHub Actions run 30672585115(格式、单测、Lint、Coverage、License Header)与 CodeRabbit 均通过。

Return the byte count accepted by the flate and snappy writers when their subsequent flush fails. This preserves the delivery error while honoring io.Writer byte-count semantics and avoids inviting callers to resend already-consumed input.

Constraint: Keep compression, flushing, locking, and error wrapping behavior unchanged.

Confidence: High; both implementations failed the new regression test before the change and pass after the two-line production fix.

Scope-risk: Limited to error-path byte counts for compressed connection writes.

Tested: Windows targeted test count=10; WSL Go 1.25 targeted test -race count=10; go test ./...; go vet ./transport ./util; go build ./transport ./util; gofmt -d; git diff --check.

Not-tested: GitHub CI and CodeRabbit on this new commit are pending.

Co-authored-by: OmX <omx@oh-my-codex.dev>
@AlexStocks

Copy link
Copy Markdown
Owner Author

补充收口 CodeRabbit review 4826439445 中未形成行内 thread 的 Flush 字节计数建议:

  • 341c276 将 flate/snappy wrapper 在底层 writer 已消费输入、随后 Flush 失败时的返回值从 0, err 修正为 n, err,保留错误包装和锁/flush 行为。
  • TestWriteFlushersReturnConsumedBytesOnFlushError 在修复前对 flate、snappy 两条路径均以 got 0, want 7 红灯失败;修复后 Windows 连续 10 次及 WSL Go 1.25 -race 连续 10 次通过。
  • WSL go test ./...go vet ./transport ./utilgo build ./transport ./util 通过;最终 GitHub Actions run 30673280682 的格式、单测、Lint、Coverage 与 License Header 全部通过。

@AlexStocks
AlexStocks merged commit cc9909d into master Aug 1, 2026
3 checks passed
NeverENG added a commit to NeverENG/getty that referenced this pull request Aug 11, 2026
- rename isCompressed to codecEnabled; SetCompressType(CompressNone)
  still installs a flate codec stream, so the state must not be named
  by compression level
- keep batch [][]byte sends on the codec writer (single flush) so a
  codec connection never mixes coded and raw frames (AlexStocks#102/AlexStocks#107)
- add wire-format regression tests: mixed []byte and [][]byte sends
  over a CompressNone codec pair, plus raw writev coverage when
  SetCompressType was never called
- make TestTCPClient's dummy peer a TCP discard server so codec frames
  do not make an HTTP peer close mid-test
- run make fmt; imports are formatter-clean
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment