Skip to content

feat(websocket): add event helper package#1917

Merged
ReneWerner87 merged 16 commits into
mainfrom
codex/websocket-event-helper
May 29, 2026
Merged

feat(websocket): add event helper package#1917
ReneWerner87 merged 16 commits into
mainfrom
codex/websocket-event-helper

Conversation

@ReneWerner87
Copy link
Copy Markdown
Member

@ReneWerner87 ReneWerner87 commented May 11, 2026

Summary by CodeRabbit

  • New Features

    • New plain WebSocket event-bus helper: event listeners, per-connection attributes/UUIDs, emit/broadcast APIs, draining and graceful shutdown.
  • Documentation

    • Migration guidance to the new WebSocket event package, with examples and operational notes.
  • Deprecated

    • A compatibility shim exposes legacy plain event-bus behavior for gradual migration.
  • Bug Fixes

    • Server upgrade behavior hardened to enable connection hijacking support exactly once per server instance.
  • Tests

    • Extensive integration/unit tests covering event delivery, lifecycle, and error scenarios.

Review Change Stack

@ReneWerner87 ReneWerner87 force-pushed the codex/websocket-event-helper branch from 9cbeb51 to af8bd91 Compare May 11, 2026 16:23
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new event subpackage to the Fiber WebSocket middleware, providing an event-bus helper for plain WebSockets with features like connection pooling and event-driven communication. The code review identified several significant issues: the read method's polling mechanism is inefficient and potentially blocks context cancellation, the Close method fails to trigger proper resource cleanup, and the retry logic in the send goroutine may compromise message ordering. Furthermore, unsafe type assertions in attribute retrieval methods pose a risk of runtime panics, and the reviewer suggested simplifying slice copying logic for better readability.

Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
@ReneWerner87 ReneWerner87 force-pushed the codex/websocket-event-helper branch from af8bd91 to 66bed97 Compare May 11, 2026 16:28
@ReneWerner87
Copy link
Copy Markdown
Member Author

/gemini review

@ReneWerner87
Copy link
Copy Markdown
Member Author

@copilot review

@ReneWerner87
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new v3/websocket/event package providing a plain WebSocket event-bus helper, alongside a v3/socketio/legacy shim to facilitate migration for users of older Socket.IO implementations. It also refactors the base websocket package to safely manage hijacked connections. Review feedback highlights several critical areas for improvement: potential runtime panics in attribute getters due to unsafe type assertions, a race condition in the legacy configuration synchronization, and performance overhead in the read loop caused by unnecessary polling. Additionally, it is recommended to make the message queue size configurable and to optimize the retry logic to prevent excessive goroutine spawning.

Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/socketio/legacy/legacy.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5c8377d-76f2-4a5b-9701-8eebf74c0644

📥 Commits

Reviewing files that changed from the base of the PR and between 1a2304b and 3954366.

📒 Files selected for processing (3)
  • v3/websocket/event/README.md
  • v3/websocket/event/event.go
  • v3/websocket/event/event_test.go
✅ Files skipped from review due to trivial changes (1)
  • v3/websocket/event/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • v3/websocket/event/event.go
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (4)
v3/websocket/event/event_test.go (4)

61-69: LGTM!


721-770: LGTM!


796-821: LGTM!


772-794: ⚡ Quick win

kws.send terminates on cancel() (via ctx.Done()).

v3/websocket/event/event.go’s outer for { select { ... } } includes case <-ctx.Done(): return (and case <-kws.done: return), so after the message is dropped and EventError fires, the goroutine won’t block forever on an empty kws.queue and should not trip goleak.


📝 Walkthrough

Walkthrough

Implements a plain WebSocket event-bus in v3/websocket/event (UUID-based pool, global/per-connection listeners, buffered delivery, draining/CloseAll), adds a deprecated v3/socketio/legacy shim that forwards the old API, updates docs, synchronizes websocket handler server config, and adds extensive tests.

Changes

WebSocket Event-Bus, Legacy Compatibility, and Documentation

Layer / File(s) Summary
Top-level README and migration notes
v3/socketio/README.md, v3/socketio/legacy/README.md, v3/websocket/README.md
Adds migration guidance and a new legacy README pointing to websocket/event and documenting temporary shim usage and tuning guidance.
Legacy shim types and aliases
v3/socketio/legacy/legacy.go
Deprecated legacy package with type aliases mapping Websocket and EventPayload to websocket/event types and re-exported constants/errors.
Legacy shim forwarding APIs
v3/socketio/legacy/legacy.go
Thin deprecated wrappers (New, On, EmitToList, EmitTo, Broadcast, Fire) delegating to websocket/event.
Legacy shim integration test
v3/socketio/legacy/legacy_test.go
Integration test that verifies legacy-mode websocket route echoes legacy-prefixed messages and includes a dial-retry helper.
WebSocket handler server-level synchronization
v3/websocket/websocket.go
Adds module-level sync to set fasthttp.Server.KeepHijackedConns once per server via a mutex-protected map and helper.
Event package documentation
v3/websocket/event/README.md
Comprehensive docs for websocket/event: API surface, examples, config, listener scope, sending semantics, thread-safety, and shutdown patterns.
Frame/event constants and tuning variables
v3/websocket/event/event.go
RFC6455 frame opcodes, event name constants, exported errors, and tuning variables (PongTimeout, RetrySendTimeout, MaxSendRetry, SendQueueSize, deprecated ReadTimeout).
EventPayload and Config resolution
v3/websocket/event/event.go
Defines EventPayload, Config, and resolves per-connection immutable settings from provided config and legacy defaults.
Websocket wrapper and connection pool
v3/websocket/event/event.go
Websocket struct with connection state, outbound queue, attributes, per-connection listeners, and global UUID->connection pool and listener registries.
Fiber handlers and connection startup
v3/websocket/event/event.go
New and NewWithConfig handlers upgrade requests, register UUIDs, invoke callbacks, fire connect, and start background goroutines.
UUID and attribute management
v3/websocket/event/event.go
GetUUID, SetUUID (with remapping & duplication detection), SetAttribute, and typed getters.
EmitTo / EmitToList / Broadcast / Fire APIs
v3/websocket/event/event.go
Instance and package-level targeted emission/broadcast APIs with differing error-event semantics.
Emit, Close, liveness, and send queue
v3/websocket/event/event.go
Frame emission, RFC6455 close handling, IsAlive, ping loop, outbound queueing, send loop with bounded retries and queue draining.
Connection run, read loop, and disconnection handling
v3/websocket/event/event.go
run lifecycle, read loop emitting EventMessage and handling control frames, disconnected cleanup and event firing.
Event dispatch and listener infrastructure
v3/websocket/event/event.go
Global/per-connection listener merging, attribute snapshotting, cloning payload bytes, panic-recovered callback invocation, and On/Off APIs.
Draining and CloseAll
v3/websocket/event/event.go
Atomic draining flag with IsDraining/Drain and CloseAll that concurrently closes active connections and force-closes on timeout.
Test helpers and mocks
v3/websocket/event/event_test.go
TestMain with goleak ignores, HandlerMock/WebsocketMock, resetState, createWS, upgradeMiddleware, and dial helpers.
Integration and unit tests
v3/websocket/event/event_test.go
Extensive tests: real client delivery, Fire/Broadcast/EmitTo/EmitToList, attribute getters, SetUUID behavior, lifecycle, draining, ping interval, read-timeout/oversized frames, panic recovery, payload copy semantics, and exact-once disconnect/error events.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

📒 Documentation

Suggested reviewers

  • gaby
  • sixcolors
  • efectn

Poem

🐰 I hopped through websockets, nibbling on bytes,
UUIDs in my basket, bouncing through nights.
A legacy bridge keeps old code warm,
New event streams now dance in form.
Hooray for graceful closes and panic-safe flights!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 PR title 'feat(websocket): add event helper package' accurately and concisely describes the primary change—introducing a new event helper package to the websocket module with per-connection listeners, package-level APIs, and improved event semantics.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/websocket-event-helper

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
v3/websocket/event/event.go (1)

526-563: 🏗️ Heavy lift

Ticker-based read loop is inefficient.

The read function uses a ticker with ReadTimeout (10ms default) to poll for messages, which burns CPU cycles and introduces latency. WebSocket ReadMessage is blocking and can be interrupted with SetReadDeadline or by closing the connection. Consider blocking on ReadMessage directly and using context cancellation or connection closure to exit the loop.

Possible approach (blocking ReadMessage)
func (kws *Websocket) read(ctx context.Context) {
	for {
		if !kws.hasConn() {
			select {
			case <-ctx.Done():
				return
			case <-time.After(ReadTimeout):
				continue
			}
		}

		mType, msg, err := kws.Conn.ReadMessage() // blocking

		// Handle context cancellation by checking after read
		select {
		case <-ctx.Done():
			return
		default:
		}

		if mType == PingMessage {
			kws.fireEvent(EventPing, nil, nil)
			continue
		}
		// ... rest of handling
	}
}

This approach blocks on ReadMessage and only checks for context cancellation when needed, significantly reducing CPU usage.

🤖 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 `@v3/websocket/event/event.go` around lines 526 - 563, The read loop currently
polls with a ticker; replace it with a blocking ReadMessage loop in the read
method: stop using timeoutTicker, loop calling kws.Conn.ReadMessage() directly
(but only when kws.hasConn() is true), handle
PingMessage/PongMessage/CloseMessage and error exactly as now (calling
kws.fireEvent or kws.disconnected), and use context cancellation to break the
loop by either setting a read deadline via Conn.SetReadDeadline(ctxDeadline)
each iteration or by closing the connection from another goroutine when ctx is
done so ReadMessage unblocks; keep checks to skip reads when !kws.hasConn() and
still return on ctx.Done(). Ensure you update references to read, hasConn,
Conn.ReadMessage, SetReadDeadline (or connection close), fireEvent and
disconnected accordingly.
🤖 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 `@v3/socketio/legacy/legacy_test.go`:
- Around line 40-50: The test starts the server in a goroutine using
app.Listener(ln) but immediately attempts a client Dial via
websocket.Dialer.Dial("ws://"+ln.Addr().String(), nil), causing a race; ensure
the client waits for the server to be ready by adding synchronization—e.g. have
the goroutine signal on a ready channel (send after app.Listener(ln) returns or
once the listener is accepting) and block the test until that channel is
closed/received, or implement a short retry/backoff loop around dial attempts
(retry websocket.Dialer.Dial using ln.Addr().String() until success with a
timeout); reference the goroutine that calls app.Listener(ln) and the dial call
that invokes websocket.Dialer.Dial to locate where to add the
synchronization/retry.
- Around line 8-12: The dependency github.com/gofiber/contrib/v3/websocket
(imported as fws in the test imports) is pinned to v1.0.0 and must be updated to
v1.1.2; open go.mod and change the module version for
github.com/gofiber/contrib/v3/websocket to v1.1.2, then run go get
github.com/gofiber/contrib/v3/websocket@v1.1.2 and go mod tidy to update the
lockfile and ensure imports (e.g., the fws alias in legacy_test.go) continue to
resolve against the new version.

In `@v3/websocket/event/event.go`:
- Around line 320-329: GetStringAttribute currently does an unchecked type
assertion value.(string) which can panic if the stored attribute isn't a string;
change it to use a checked assertion (s, ok := value.(string)) after reading
from kws.attributes inside the mutex-protected block and return s only if ok is
true, otherwise return the empty string (optionally consider logging the type
mismatch), referencing the GetStringAttribute method, kws.attributes map and
kws.mu mutex.
- Around line 309-318: GetIntAttribute currently does an unchecked type
assertion value.(int) which can panic if kws.attributes[key] holds a non-int;
change it to use a checked assertion (v, ok := value.(int)) and return v when
ok, otherwise return 0 (or a default) to avoid panics — update the
GetIntAttribute method on the Websocket type that reads from kws.attributes
(while holding kws.mu.RLock) to perform the safe assertion and handle the
mismatch gracefully (optionally log or comment the fallback).

---

Nitpick comments:
In `@v3/websocket/event/event.go`:
- Around line 526-563: The read loop currently polls with a ticker; replace it
with a blocking ReadMessage loop in the read method: stop using timeoutTicker,
loop calling kws.Conn.ReadMessage() directly (but only when kws.hasConn() is
true), handle PingMessage/PongMessage/CloseMessage and error exactly as now
(calling kws.fireEvent or kws.disconnected), and use context cancellation to
break the loop by either setting a read deadline via
Conn.SetReadDeadline(ctxDeadline) each iteration or by closing the connection
from another goroutine when ctx is done so ReadMessage unblocks; keep checks to
skip reads when !kws.hasConn() and still return on ctx.Done(). Ensure you update
references to read, hasConn, Conn.ReadMessage, SetReadDeadline (or connection
close), fireEvent and disconnected accordingly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63d4d93c-f650-44ba-8a0a-2524226f253c

📥 Commits

Reviewing files that changed from the base of the PR and between 7b86e04 and 66bed97.

⛔ Files ignored due to path filters (2)
  • v3/websocket/go.mod is excluded by !**/*.mod
  • v3/websocket/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (9)
  • v3/socketio/README.md
  • v3/socketio/legacy/README.md
  • v3/socketio/legacy/legacy.go
  • v3/socketio/legacy/legacy_test.go
  • v3/websocket/README.md
  • v3/websocket/event/README.md
  • v3/websocket/event/event.go
  • v3/websocket/event/event_test.go
  • v3/websocket/websocket.go
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: copilot
🧰 Additional context used
🪛 LanguageTool
v3/socketio/legacy/README.md

[grammar] ~5-~5: Ensure spelling is correct
Context: --- id: socketio-legacy --- # SocketIO Legacy Event Shim Compatibility shim f...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (23)
v3/websocket/websocket.go (2)

82-95: LGTM!


159-159: LGTM!

v3/websocket/event/event.go (9)

16-69: LGTM!


71-143: LGTM!


145-224: LGTM!


226-257: LGTM!


259-266: LGTM!

Also applies to: 291-307


267-289: LGTM!


331-421: LGTM!


473-500: LGTM!


422-446: LGTM!

Also applies to: 447-458, 460-471, 502-524, 565-634

v3/websocket/event/README.md (1)

1-95: LGTM!

v3/websocket/README.md (1)

13-14: LGTM!

v3/websocket/event/event_test.go (1)

1-397: LGTM!

v3/socketio/legacy/legacy.go (1)

1-107: LGTM!

v3/socketio/legacy/README.md (1)

1-34: LGTM!

v3/socketio/README.md (1)

13-14: LGTM!

v3/socketio/legacy/legacy_test.go (6)

1-13: LGTM!


15-22: LGTM!


24-29: LGTM!


44-52: LGTM!


54-59: LGTM!


31-34: All required symbols are properly exported from the legacy package.

The verification confirms that On, EventPayload, Websocket, and TextMessage are all exported from v3/socketio/legacy/legacy.go (as either type aliases or constants). These symbols are correctly accessible within the test file without explicit imports since the test shares the same package scope.

Comment thread v3/socketio/legacy/legacy_test.go
Comment thread v3/socketio/legacy/legacy_test.go Outdated
Comment thread v3/websocket/event/event.go
Comment thread v3/websocket/event/event.go
Copy link
Copy Markdown
Contributor

Copilot AI commented May 12, 2026

@copilot review

Reviewed and addressed in af4409d. Here's a summary of what was found and fixed:

Bugs fixed:

  • GetIntAttribute / GetStringAttribute used bare type assertions (value.(int) / value.(string)) that would panic if the attribute was stored with a different type. Changed to safe two-value assertions that return the zero value on mismatch.
  • safeListeners.set referenced the global listeners.list instead of the receiver l.list. Fixed to use l.list.
  • EmitTo (both method and package-level) had a redundant pool.contains() call immediately after pool.get() — the get already validates existence. Removed the redundant check and unified error handling.
  • queueLength() was dead code (defined but never called). Removed.

Tests updated to cover safe type coercion: GetIntAttribute and GetStringAttribute now verify that type-mismatched and missing keys return zero values rather than panicking.

Design notes (not changed):

  • payload.Data carries []byte(uuid) in error events so callers can identify which connection failed — intentional.
  • The method EmitTo fires an EventError on failures (has a connection context to dispatch on); the package-level EmitTo just returns the error (no connection context) — intentional asymmetry.

@ReneWerner87 ReneWerner87 force-pushed the codex/websocket-event-helper branch from 70b3432 to 4e85f0a Compare May 12, 2026 08:26
@ReneWerner87 ReneWerner87 force-pushed the codex/websocket-event-helper branch from 4e85f0a to b0e9c2c Compare May 12, 2026 09:21
ReneWerner87 and others added 10 commits May 12, 2026 12:49
…ettings

Adds an event.Config struct and event.NewWithConfig entry point so callers can
configure ping interval, idle timeout, write timeout, max message size, send
queue size, send retries and a recover handler per instance. The existing
event.New keeps its signature and delegates to NewWithConfig with a zero
Config.

At connection upgrade the package vars (PongTimeout, RetrySendTimeout,
MaxSendRetry, SendQueueSize) are read exactly once into an immutable
per-connection settings snapshot. Goroutines (pong, send) now read
kws.settings instead of the bare package vars, so later mutations of the
globals can no longer race with running connections.

The done channel buffer of 1 is dropped: close() works regardless and a
buffered signal channel is misleading.

No behavior change for existing callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A panic in a user On() callback used to abort the read or send goroutine
before disconnected() ran, leaving the pool entry and worker goroutines
behind. The pooled *Conn could then be recycled while the helper still
held a reference. Now fireEvent invokes every callback through a small
trampoline that recovers; the optional Config.RecoverHandler receives the
event name and recovered value for telemetry.

While here, clone the inbound []byte once before fan-out so listeners that
retain payload.Data are not exposed to the read buffer being reused for
the next frame.

Adds TestListenerPanicIsRecovered and
TestEventPayloadDataIsIndependentOfReadBuffer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before this change ReadMessage was called without SetReadLimit, so a single
client could stream a multi-GB frame and OOM the process. run() now
applies kws.settings.maxMessageSize to the underlying *Conn before any
read happens. The default is 1 MiB; callers opt in to larger frames via
Config.MaxMessageSize.

Adds TestReadLimitRejectsOversizedFrame which configures a 16-byte cap and
asserts EventDisconnect with a non-nil error after a 1 KiB write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… on Pong

The old pong() goroutine sent unsolicited Pong frames on a ticker, which is
neither a Ping nor a real heartbeat: dead peers were only detected when
the next write attempt failed, and many proxies (Cloudflare, Envoy) reset
their idle timers only on data or Ping frames. The read loop had no
deadline at all, so an idle connection wedged three goroutines forever.

This commit:

  - rewrites pong() to send PingMessage via WriteControl with a write
    deadline (and exits on done so it cannot outlive the connection);
  - installs SetPongHandler that refreshes the read deadline and fires
    EventPong, plus SetPingHandler that mirrors the peer's data into a
    Pong response and fires EventPing;
  - sets ReadDeadline at run() start so a silent peer surfaces a timeout
    error and gets disconnected;
  - rewrites read() to drop the dead PingMessage/PongMessage dispatch
    branches (control frames go through the library hooks), distinguishes
    orderly close (1000/1001) from transport errors via IsCloseError, and
    polls ctx/done on each iteration.

PongTimeout keeps its name for backwards compatibility but its godoc is
rewritten and marked Deprecated in favour of Config.PingInterval.

Adds TestPingIsSentAtInterval and TestReadDeadlineFiresDisconnectOnSilentPeer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…shutdown order

Three related transport fixes that all touch the close path:

  - send() now sets a write deadline before each WriteMessage and, on
    error, drains the queue and disconnects rather than spinning. Without
    this, a slow reader could wedge the send goroutine forever while
    Broadcast/EmitTo from other connections kept enqueuing.

  - Close() no longer writes a non-RFC payload of "Connection closed"
    bytes directly. RFC 6455 §5.5.1 mandates that close frames start with
    a uint16 status code; peers MAY drop the connection with 1002
    otherwise. A new writeClose helper formats the frame via
    FormatCloseMessage, caps the reason to 123 bytes (125 control-frame
    limit minus 2 for the code), and uses WriteControl with a deadline so
    shutdown bypasses the queue entirely.

  - run() reorders shutdown to <-done -> cancelFunc -> wg.Wait ->
    closeConn. Previously closeConn raced with an in-flight WriteMessage
    in the send goroutine. closeConn now nils kws.Conn under the mutex so
    accidental external use panics on a clearly invalid value instead of
    corrupting a recycled pooled *Conn.

Adds TestCloseSendsFormatCloseMessage and TestCloseConnNilsConnField.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Fiber app.Shutdown previously hung on the event helper because every
upgrade handler is blocked in run() until the connection's done channel
closes, and nothing iterated the in-process pool to send a server-initiated
close. Clients saw a TCP reset rather than a 1001 Going Away frame.

This commit adds:

  - event.Drain() / event.IsDraining() so an upgrade middleware can
    cooperatively refuse new connections during a graceful shutdown;
  - event.CloseAll(ctx, code, reason) that walks the pool snapshot, sends
    the requested close frame via writeClose, fires EventClose, then
    waits for run() to exit. If ctx expires first, every remaining
    connection is force-closed via closeConn and ctx.Err is returned.

README documents the Fiber Hooks.OnShutdown wiring and the new Config
knobs introduced in earlier commits (PingInterval, ReadIdleTimeout,
WriteTimeout, MaxMessageSize, SendQueueSize, MaxSendRetry,
RetrySendTimeout, RecoverHandler).

Adds TestDrainFlag and TestCloseAllSendsGoingAway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a TestMain that runs goleak.VerifyTestMain against the event helper
test suite so regressions in connection cleanup (panic recovery, read
deadline, send-side shutdown ordering) are caught instead of slipping
through.

Known fasthttp worker pool and server-date refresher goroutines park in
time.Sleep after Server.Shutdown returns, so they are matched by
IgnoreAnyFunction rather than IgnoreTopFunction.

Verified locally with go test -race -count=3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the previous P1 commits, three helpers became unreachable:

  - (*Websocket).tryWrite: only used by the old Close() path that has
    been replaced by writeClose using WriteControl;
  - sendQueueSize: replaced by per-connection settings snapshot in the
    settings refactor;
  - undrain: only the tests reset the drain flag, so they now do it
    inline with draining.Store(false).

golangci-lint v2.6.2 with --tests=false flagged all three. Removing
them keeps the lint job clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
goleak is a direct test dependency of v3/websocket/event, not indirect,
and the v3/socketio module sees it via the local replace directive on
v3/websocket. go mod tidy in both modules surfaces these as the only
diff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ReneWerner87 ReneWerner87 linked an issue May 12, 2026 that may be closed by this pull request
3 tasks
@ReneWerner87 ReneWerner87 marked this pull request as ready for review May 18, 2026 06:32
@ReneWerner87 ReneWerner87 requested a review from a team as a code owner May 18, 2026 06:32
@ReneWerner87 ReneWerner87 requested review from Copilot, efectn, gaby and sixcolors and removed request for a team May 18, 2026 06:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new event subpackage under v3/websocket that provides a plain WebSocket event-bus helper (extracted from the legacy socketio package as a re-architected, configurable, leak-tested implementation), plus a thin deprecated compatibility shim at v3/socketio/legacy so existing consumers can migrate. Also adds a small helper in the core websocket middleware so KeepHijackedConns is enabled exactly once per fasthttp.Server and updates README/docs to point users to the new package.

Changes:

  • New v3/websocket/event package implementing per-connection settings, ping/pong liveness, send queue with retry, graceful Drain/CloseAll, and panic-recovering listeners, with comprehensive tests using goleak.
  • New v3/socketio/legacy shim re-exporting the event API for backwards compatibility, plus README updates redirecting users.
  • Concurrency-safe ensureKeepHijackedConns(server) helper replacing the inline read-modify-write on server.KeepHijackedConns; minor refactor of socketio/polling.go cookie iteration; go.mod/sum updates for goleak and google/uuid.

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
v3/websocket/websocket.go Adds ensureKeepHijackedConns map+mutex helper and routes the upgrade handler through it.
v3/websocket/README.md Points readers to the new event subpackage.
v3/websocket/go.mod / go.sum Promotes google/uuid to direct dep, adds goleak and stretchr/objx.
v3/websocket/event/event.go New event-bus implementation with Config, lifecycle, pool, listeners, graceful shutdown.
v3/websocket/event/event_test.go Extensive tests including goleak, parallel dialers, ping/read-deadline, panic recovery.
v3/websocket/event/README.md Documentation for the new package.
v3/socketio/README.md Migration note pointing to the new event package and legacy shim.
v3/socketio/legacy/legacy.go Thin deprecated alias layer re-exporting wsevent types/functions.
v3/socketio/legacy/legacy_test.go Smoke test verifying the shim round-trips a message.
v3/socketio/legacy/README.md Migration documentation for the shim.
v3/socketio/polling.go Replaces VisitAllCookie callback with Cookies() range iterator.
v3/socketio/go.mod / go.sum Bumps websocket dep version and adds goleak.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event.go Outdated
Comment thread v3/websocket/event/event_test.go
Comment thread v3/websocket/event/event.go
Comment thread v3/websocket/event/event.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 `@v3/websocket/event/event.go`:
- Around line 940-947: In the ctx.Done() branch inside CloseAll, after casting
to *Websocket and calling kws.closeConn(), also mark the connection as
disconnected so it isn't considered alive or left in the pool: set kws.isAlive =
false (or call the Websocket disconnect/mark method if one exists) and remove
its UUID from pool (e.g., call pool.remove/erase using the Websocket's ID) so
subsequent emits won't treat the closed socket as alive.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c2d7602-26cc-4305-b0be-53593fdb4e9b

📥 Commits

Reviewing files that changed from the base of the PR and between 66bed97 and d103993.

⛔ Files ignored due to path filters (4)
  • v3/socketio/go.mod is excluded by !**/*.mod
  • v3/socketio/go.sum is excluded by !**/*.sum, !**/*.sum
  • v3/websocket/go.mod is excluded by !**/*.mod
  • v3/websocket/go.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (10)
  • v3/socketio/README.md
  • v3/socketio/legacy/README.md
  • v3/socketio/legacy/legacy.go
  • v3/socketio/legacy/legacy_test.go
  • v3/socketio/polling.go
  • v3/websocket/README.md
  • v3/websocket/event/README.md
  • v3/websocket/event/event.go
  • v3/websocket/event/event_test.go
  • v3/websocket/websocket.go
✅ Files skipped from review due to trivial changes (4)
  • v3/socketio/polling.go
  • v3/socketio/README.md
  • v3/socketio/legacy/README.md
  • v3/websocket/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • v3/websocket/websocket.go
  • v3/socketio/legacy/legacy_test.go
  • v3/websocket/event/README.md
📜 Review details
🔇 Additional comments (1)
v3/websocket/event/event_test.go (1)

346-346: No action needed – Go version 1.25.0 supports range-over-integer loops.

The module's go directive specifies 1.25.0, which fully supports the range-over-integer syntax used in this code. No changes required.

Comment thread v3/websocket/event/event.go
@ReneWerner87
Copy link
Copy Markdown
Member Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new plain WebSocket event helper subpackage websocket/event to handle legacy event-bus behavior, along with a compatibility shim in socketio/legacy to ease migration. It also updates dependencies and introduces thread-safe server configuration in websocket.go. However, a critical compilation and logic error was identified in v3/socketio/polling.go where iterating over Cookies() incorrectly treats the loop variables as key-value pairs instead of index and struct, which must be resolved.

Comment thread v3/socketio/polling.go
ReneWerner87 and others added 3 commits May 29, 2026 08:52
…-helper

# Conflicts:
#	v3/socketio/go.mod
#	v3/socketio/go.sum
#	v3/websocket/go.mod
#	v3/websocket/go.sum
- Broadcast and disconnected now read the UUID via GetUUID() under the
  read lock instead of touching kws.UUID directly, removing a data race
  with SetUUID.
- CloseAll's force-close path marks each connection disconnected before
  Conn.Close so a timed-out shutdown cannot leave stale alive entries in
  the pool.
- SetUUID validates duplication before mutating kws.UUID, dropping the
  rollback dance.
- Clarify EmitTo/EmitToList godocs: the method forms fire EventError, the
  package-level forms do not.
- Rewrite TestWebsocketCloseDoesNotBlockOnFullQueue to exercise write()
  on a full queue after Close, the path that actually interacts with the
  queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses a second review round focused on documentation and the global
listener footgun.

- Add per-connection listeners via (*Websocket).On / (*Websocket).Off that
  fire alongside the process-global listeners and are discarded when the
  connection disconnects. Export the EventCallback type so the On signature
  is usable from go doc.
- Add package-level Off(event) as the missing counterpart to On.
- Fire EventError(ErrorInvalidConnection) when send() drops an outbound
  message after exhausting retries instead of dropping it silently.
- De-duplicate EventError: the method EmitToList/Broadcast no longer fire a
  second EventError, since the EmitTo they delegate to already fires one per
  failed UUID.
- Rewrite the README: method-vs-package send semantics, global vs
  per-connection listeners, SetUUID/GetUUID, thread-safety, the IsDraining
  upgrade gate, custom events, and EventConnect/EventDisconnect timing.
  Clarify the event constant godocs accordingly.
- Add tests for Off, per-connection scoping, the dropped-message EventError,
  and the method-form EmitTo/EmitToList EventError firing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ReneWerner87 ReneWerner87 merged commit d935d13 into main May 29, 2026
14 checks passed
@ReneWerner87 ReneWerner87 deleted the codex/websocket-event-helper branch May 29, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 [Bug]: SocketIO middleware is not compatible with socket.io-client

3 participants