Repository navigation
[Bug]: Requests that arrive during server startup are never answered (socket accepts, handler not attached yet) #12215
Description
Activity
- addedbugSomething is broken or behaving incorrectly.Something is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.Issue needs maintainer review and initial categorization.
on Sep 17, 2026 Triage
Thanks for the repro — this is real, and the cause analysis is right.
What happens
NodeHttpServer.makecallsserver.listen()while the HTTP server layer is being built.server.on("request", …)/server.on("upgrade", …)are registered later, inserve(httpApp). T3 buildsHttpServerLivefirst (Layer.provideMerge(HttpServerLive)), then the heavy startup graph (SQLite, providers, routes), thenHttpRouter.serve. In that window the OS accepts TCP, Node emitsrequestwith no app handler, and the event is not replayed whenserve()finally attaches. The socket stays open until the client gives up. Nothing is logged.That matches the captured output (
early: 000 45s,late: 200 ~7ms, no server line for the early request).Still present on current
main(@effect/platform-node4.0.0-rc.112 / vendored effect-smol). Samelisten()-then-serve()split as 4.0.0-beta.103.Likely code
apps/server/src/server.ts—HttpServerLive(~L224) listens as soon as the layer is acquired;HttpRouter.serve(makeRoutesLayer)(~L785) attaches handlers later;Layer.provideMerge(HttpServerLive)(~L801) forces listen-first.apps/server/src/httpResponseErrorGuard.ts— already listens forrequest/upgradeonly to attach write-error handlers. That does not answer the request, so an early GET still hangs.apps/server/src/server.tscommandReadinessLayer(~L559) — global middleware that holds requests untilawaitCommandReady. It only runs afterserve()attaches. The intended “hold until ready” behavior already exists; it never sees the dropped request..repos/effect-smol/packages/platform/node/src/NodeHttpServer.tsmake—server.listen()(~L122) in the constructor;server.on("request" | "upgrade")(~L173) only insideserve().apps/server/src/serverRuntimeStartup.ts— headless “T3 Code server is ready” is parked until activation, and activation waits onroutesReady. Log-ready is a valid probe; TCP-accept is not.Listening oncomes fromHttpServer.withLogAddressonHttpRouter.serve, so it also means the handler is attached.
No duplicate issue. No existing Effect-TS/effect or effect-smol ticket for this split that I could find. Adjacent but different: #8442 / #9652 (second server / live data dir), #7703 (port-in-use message).
Fix direction
Do not wait on Effect for user-visible impact.
- Local (T3): on the raw Node server created in
HttpServerLive/guardHttpResponseWriteErrors, attach a temporaryrequesthandler that answers503+Retry-After(and close earlyupgrades) before Effect callslisten(). Flip it off whenHttpRouter.serveattaches the real handler (a flag is enough; after that the existingcommandReadinessLayerholds until command-ready). Do not leave a second listener that also writes 503 afterserve(). - Upstream: file/track an Effect-TS/effect-smol issue: register
request/upgradebeforelisten(), or movelisten()intoserve(). T3 cannot reorder this away —HttpServeris only provided aftermake()has already listened. - Test: HTTP GET as soon as the port accepts, assert a finite response (
503or a held200), not a client timeout. Coverupgradeas well if cheap.
Moving
HttpServerLivelater in the layer graph does not help: constructing the service islisten().Workaround
Wait for
Listening onorT3 Code server is readyin the server log (or retry curl with a short--max-time). Do not treat “TCP connect succeeds” as ready. That is enough for NixOS VM tests until the local 503 ships.Accepting as a server startup bug. Root cause is upstream Effect; T3 can and should close the window locally.
- addedacceptedfeature request acceptedfeature request acceptedvia-triageFiled through npx t3 triageFiled through npx t3 triageand removedneeds-triageIssue needs maintainer review and initial categorization.Issue needs maintainer review and initial categorization.
on Sep 17, 2026 - added a commit that references this issue
on Oct 1, 2026
Before submitting
Area
apps/server
Steps to reproduce
Uses a throwaway data dir and a spare port, so no existing state is touched:
Expected behavior
A request that connects before the server is ready is either held and served once the app is ready, or rejected quickly (e.g. connection refused or
503+Retry-After). The port should not accept connections it will never answer.Actual behavior
The port accepts connections roughly 0.5–1s before the HTTP handler is attached. Requests sent in that window never get a response, not even after the server is ready. The socket stays open until the client gives up, so a client with no timeout hangs forever. In a NixOS VM test, a plain
curlhung for the full 1h test timeout.Requests sent after "T3 Code server is ready" are fine (200 in ~7ms).
Cause, as far as I can tell: in
@effect/platform-node(NodeHttpServer.make, 4.0.0-beta.103),server.listen()runs while the layer is being built, butserver.on("request", handler)/server.on("upgrade", ...)are only registered later inserve(httpApp). The other server startup layers (SQLite migrations, provider setup, …) run in between. Node'shttp.Serveremitsrequestwith no listener, so the request is silently dropped.Possible fixes: register the
request/upgradelisteners beforelisten(), or answer503 Retry-Afteruntilserve()runs, or bring up the HTTP server layer after the heavy startup layers. The root cause is probably worth a matching issue in Effect-TS/effect-smol.Impact
Minor bug or occasional failure
Version or commit
t3 v0.0.40 (@effect/platform-node 4.0.0-beta.103)
Environment
NixOS 26.11, Linux 7.2.4, Node 24.20.0,
t3 serve(web mode, headless)Logs or stack traces
Screenshots, recordings, or supporting files
No response
Workaround
No response