Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
Uses a throwaway data dir and a spare port, so no existing state is touched:
H=$(mktemp -d); export HOME=$H T3CODE_HOME=$H/.t3
t3 serve --host 127.0.0.1 --port 38775 > $H/log 2>&1 &
# 1. wait until the port accepts TCP connections
until (exec 3<>/dev/tcp/127.0.0.1/38775) 2>/dev/null; do sleep 0.05; done
# 2. send a request right away (before "T3 Code server is ready")
curl -s -o /dev/null -w "early: %{http_code} %{time_total}s\n" --max-time 45 http://127.0.0.1:38775/ &
# 3. wait for readiness, then send a second request
until grep -q "server is ready" $H/log; do sleep 0.1; done
curl -s -o /dev/null -w "late: %{http_code} %{time_total}s\n" --max-time 5 http://127.0.0.1:38775/
waitExpected 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 curl hung 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, but server.on("request", handler) / server.on("upgrade", ...) are only registered later in serve(httpApp). The other server startup layers (SQLite migrations, provider setup, …) run in between. Node's http.Server emits request with no listener, so the request is silently dropped.
Possible fixes: register the request/upgrade listeners before listen(), or answer 503 Retry-After until serve() 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
late: 200 0.007279s
early: 000 45.001325s
# server log: nothing is logged for the early request
[14:32:13.680] INFO (#409): Listening on http://127.0.0.1:38775
T3 Code server is ready.Screenshots, recordings, or supporting files
No response
Workaround
No response
Activity
juliusmarminge commentedon 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.
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().request/upgradebeforelisten(), or movelisten()intoserve(). T3 cannot reorder this away —HttpServeris only provided aftermake()has already listened.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.
fix(server): answer requests that arrive before the HTTP handler atta…