Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

NodeHttpServer: requests on keep-alive connections hang during graceful shutdown

Chiusa Adatta ai principianti
#8,728 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

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
Ambito
api, backend

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

bug
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:

https://github.com/Effect-TS/effect/blob/bc445262eeaaba6f7f28a154023a590e193e547d/packages/platform/node/src/NodeHttpServer.ts#L169-L173

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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Effect-TS/effect

Tutte le issue di Effect-TS/effect

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.