Skip to content

fix: WebSocket RPC hang — nested Effect.gen deadlock in request fiber#109

Merged
harrryyd merged 1 commit into
mainfrom
fix/ws-rpc-hang
Jun 9, 2026
Merged

fix: WebSocket RPC hang — nested Effect.gen deadlock in request fiber#109
harrryyd merged 1 commit into
mainfrom
fix/ws-rpc-hang

Conversation

@harrryyd

@harrryyd harrryyd commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Root Cause

A nested Effect.gen (generator) inside another Effect.gen within the HTTP request fiber never starts executing. When yield* makeWsRpcContext(session) was called from the /ws route handler's generator, the inner generator body was never entered — its first yield* statement never ran. The same code worked perfectly in the startup fiber (self-test).

Fix

  1. Replaced Effect.gen with flat pipeline composition in makeWsRpcContext — uses Effect.flatMap/Effect.andThen chains instead of generator yield*.
  2. Moved Effect.context<never>() to startup — a new WsAppServices tag captures all app-level services once at boot via wsAppServicesLayer, instead of per-request.
  3. Fixed vendored Effect API mismatchesContext.TagContext.Service, removed nonexistent Effect.tapErrorCause.

Verification

  • Startup self-test (T3CODE_DEBUG_WS_CONTEXT_SELFTEST=1) passes: all 10 services load, handlers build successfully
  • Real /ws connection (authenticated session) now reaches ws.rpc handlers built and ws.rpc effect prepared — confirmed in live logs

Root cause: yield*-ing an Effect.gen from inside another Effect.gen
within the HTTP request fiber caused the inner generator to never
start executing. The first yield* statement inside makeWsRpcContext
never ran, silently hanging every /ws connection after auth.

Fix:
- Replaced Effect.gen in makeWsRpcContext with flat pipeline
  composition (Effect.flatMap/Effect.andThen chaining)
- Moved Effect.context<never>() capture to startup via a new
  WsAppServices service that pre-builds the full app-level context
  once at boot
- Fixed vendored Effect API mismatches: Context.Tag -> Context.Service,
  removed nonexistent Effect.tapErrorCause calls

Also includes layer-wiring fixes for startup services and auth
session infrastructure.
@github-actions github-actions Bot added size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jun 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fc126cf85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +719 to +720
yield* Effect.logDebug("auth.websocket upgrade using ticket", {
path: request.originalUrl,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid logging WebSocket tickets in request paths

When clients connect with the new ?wsTicket=... flow, request.originalUrl contains the bearer ticket, so these debug entries persist a live credential in server logs before/after verification. This affects every ticket-authenticated WebSocket upgrade; log a sanitized path or only the boolean hasWebSocketTicket instead of the raw URL.

Useful? React with 👍 / 👎.

Comment on lines +63 to +64
} catch {
existing = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve archive data on JSON parse failures

If the todo archive file is malformed or partially written, this catch makes appendToArchive treat it as empty and then writeFileStringAtomically overwrites the archive with only the newly archived items. In that scenario, a normal todo completion permanently discards all previously archived entries; it should fail the mutation or write to a separate recovery path instead of replacing unreadable data.

Useful? React with 👍 / 👎.

@harrryyd harrryyd merged commit bb76e2f into main Jun 9, 2026
5 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant