NodeHttpServer: requests on keep-alive connections hang during graceful shutdown
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 78/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- node.js, typescript
Direzione di ricerca
Inizia da packages/platform/node/src/NodeHttpServer.ts, intorno al finalizer alle righe 169-173, e riproduci il comportamento con lo script Node HTTP keep-alive dell'issue. Verifica che le richieste sulle connessioni esistenti vengano gestite o rifiutate mentre server.close() esaurisce le connessioni, e che il timeout di arresto e la chiusura forzata dei socket continuino a funzionare come previsto.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
What version of Effect is running?
4.0
What steps can reproduce the bug?
Node's server.close() drains connections instead of cutting them off. It stops accepting new TCP connections and closes idle ones. An existing connection with an in-flight HTTP request stays open, and if that connection has keep-alive enabled (eg from a reverse proxy before Node), that connection will stay open even after the response gets flushed. Node keeps emitting request event for every HTTP request on that connection.
NodeHttpServer.serve handles requests by listening to Node's request event. At shutdown, its finalizer removes the listener first and only then calls server.close(). So while the server drains, Node is still accepting requests on existing connections, but Effect is no longer listening. The next request on such a connection gets no response. It hangs until the client times out or gracefulShutdownTimeout forces the socket closed.
import { NodeHttpServer } from "@effect/platform-node"
import { Effect, Latch, Layer, ManagedRuntime } from "effect"
import { HttpServer, HttpServerResponse } from "effect/http"
import http from "node:http"
import net from "node:net"
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// Hold the first request in the handler until shutdown starts.
const requestStarted = Latch.makeUnsafe()
const releaseResponse = Latch.makeUnsafe()
const app = Effect.gen(function* () {
yield* requestStarted.open
yield* releaseResponse.await
return HttpServerResponse.text("ok")
})
const server = http.createServer()
server.keepAliveTimeout = 5000
const runtime = ManagedRuntime.make(
HttpServer.serve(app).pipe(
Layer.provide(NodeHttpServer.layer(() => server, {
host: "127.0.0.1",
port: 0,
gracefulShutdownTimeout: "3 seconds"
}))
)
)
await runtime.context()
// One TCP socket for both requests.
const { port } = server.address()
const socket = net.connect(port, "127.0.0.1")
let responseData = ""
let socketClosed = false
const firstResponse = new Promise((resolve) => {
socket.on("data", (chunk) => {
responseData += chunk
resolve()
})
})
socket.on("close", () => {
socketClosed = true
})
await new Promise((resolve) => socket.once("connect", resolve))
// Start request 1, then dispose the runtime while it is in flight.
socket.write("GET /a HTTP/1.1\r\nHost: localhost\r\n\r\n")
await Effect.runPromise(requestStarted.await)
const disposed = runtime.dispose()
// Wait for Effect's finalizer to call server.close().
while (server.listening) {
await sleep(1)
}
// Let request 1 finish while the server drains.
releaseResponse.openUnsafe()
await firstResponse
console.log(
"first answered:", responseData.includes("HTTP/1.1 200"),
"| closed:", socketClosed
)
// Send request 2 on the same socket, before the drain timeout.
socket.write("GET /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
await sleep(800)
const responseCount = (responseData.match(/HTTP\/1.1 200/g) ?? []).length
console.log("second answered:", responseCount === 2)
socket.destroy()
await disposed
What is the expected behavior?
During shutdown draining, the HTTP server should either (1) handle them or (2) reject new requests (on already accepted TCP connections). Right now, those connections just hang.
What do you see instead?
first answered: true | closed: false
second answered: false
Request 2 is never answered. A reverse proxy that reuses the connection waits until its own response timeout. Node's requestTimeout doesn't cap this wait because it only limits how long the server takes to receive a request.
Additional information
Cause: the listeners are removed before draining starts:
Suggested fix: detach the listeners after draining finishes:
yield* Scope.addFinalizerExit(serveScope, () =>
Effect.ensuring(
preemptiveShutdown,
Effect.sync(() => {
server.off("request", handler)
server.off("upgrade", upgradeHandler)
})
)
)
server.close() still refuses new TCP connections. The shutdown timeout and forced socket closing work as before.
- Lingua principale
- TypeScript
- Stelle
- 16.7k
- Fork
- 808
- Merge medio
- 8h 41m
- PR unite (30g)
- 711
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di Effect-TS/effect
-
BrowserWorkerRunner: port finalizer throws when the worker global has no close() (Bun)Forse già presa @santiago-ramos-02 l’ha presa 18 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
Effect-TS/effect#8635 · 3 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Support {self: this} for fnUntracedForse di nuovo libera @ArjunCodess l’ha presa 29 giorni fa e non c’è nessuna pull request aperta. Apertaenhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
Effect-TS/effect#8101 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Effect.callback keeps state from finished suspensions alive until the next full GCForse già presa @effect-bot l’ha presa oggi. Aperta
Effect-TS/effect#9497 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Replay-enabled PubSub keeps every published value alive until the next full GCForse già presa @effect-bot l’ha presa oggi. Aperta
Effect-TS/effect#9496 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Socket.readerString corrupts UTF-8 characters split across TCP chunksForse già presa @jayhemnani9910 l’ha presa oggi. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di Effect-TS/effect
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
paperclipai/paperclip#16785 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
code-yeongyu/senpi#3661 ·
I maintainer di solito rispondono entro 1 giorno
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 78/100
lingdojo/kana-dojo#32731 · 1 commento · 5 reazioni ·
I maintainer di solito rispondono entro 1 giorno
-
bug status: needs triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
mastra-ai/mastra#27538 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
Yeachan-Heo/oh-my-codex#3822 ·
I maintainer di solito rispondono entro 1 giorno