{"record":{"id":"a2c00e7212d56467","repo":"denoland/deno","slug":"already-closed","errorCode":null,"errorMessage":"Already closed","messagePattern":"Already closed","errorType":"exception","errorClass":"Deno.errors.Http","httpStatus":null,"severity":"error","filePath":"ext/http/00_serve.ts","lineNumber":233,"sourceCode":"    this.#external = null;\n  }\n\n  get [_upgraded]() {\n    return this.#upgraded;\n  }\n\n  _throwIfUpgraded() {\n    if (this.#upgraded) {\n      throw new Deno.errors.Http(\"Already upgraded\");\n    }\n  }\n\n  _wantsUpgrade(upgradeType) {\n    if (this.#upgraded) {\n      throw new Deno.errors.Http(\"Already upgraded\");\n    }\n    if (this.#external === null) {\n      throw new Deno.errors.Http(\"Already closed\");\n    }\n\n    if (upgradeType == \"upgradeWebSocket\") {\n      const external = this.#external;\n\n      this.url();\n      this.headerList;\n      this.remoteAddr;\n      this.close();\n\n      this.#upgraded = true;\n\n      return op_http_upgrade_websocket_next(external);\n    }\n  }\n\n  url() {\n    if (this.#urlValue !== undefined) {","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/denoland/deno/blob/89f33cbef296a2b287f323d42de54c871fa69c77/ext/http/00_serve.ts#L215-L251","documentation":"Before performing an upgrade, the serve request context checks that it is still live: once close() has run (response finished, request aborted, or handler completed), the native external handle is released (#external = null). Attempting an upgrade afterwards throws Deno.errors.Http 'Already closed' because the underlying connection is gone.","triggerScenarios":"Awaiting something (a promise, timeout, or another request) inside the handler and only then calling upgradeWebSocket, after the response was already committed or the request was closed; upgrading in a detached async continuation after the handler returned.","commonSituations":"Async handler flows that respond first and upgrade later; cleanup/timeout races where the framework closed the request; websocket fallback logic that runs too late; long-running handlers whose client disconnected first.","solutions":["Perform the upgrade synchronously during request handling, before any awaits that can complete the response","Branch on the upgrade header early and return the upgrade response from that branch","Catch Deno.errors.Http for late upgrades and treat it as a lost connection, not a crash"],"exampleFix":"// before\nasync function handler(req, info) {\n  await doSlowWork();\n  return info.upgradeWebSocket(req).response; // Already closed\n}\n\n// after\nasync function handler(req, info) {\n  if (wantsWs(req)) {\n    const { socket, response } = info.upgradeWebSocket(req); // upgrade first\n    doSlowWork().then(() => socket.send(\"ready\"));\n    return response;\n  }\n  await doSlowWork();\n  return new Response(\"ok\");\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const { socket, response } = info.upgradeWebSocket(req);\n} catch (err) {\n  if (err instanceof Deno.errors.Http && err.message === \"Already closed\") {\n    // request finished before upgrade; nothing to salvage, close quietly\n    return new Response(\"upgrade too late\", { status: 400 });\n  } else throw err;\n}","preventionTips":["Upgrade synchronously during request handling, before awaits that can finish the response","Branch on the Upgrade header first and return the upgrade response immediately","Treat 'Already closed' as a lost connection, not a retriable error"],"tags":["http","serve","upgrade","lifecycle"],"backgroundTag":null,"analyzedSha":"89f33cbef296a2b287f323d42de54c871fa69c77","analyzedAt":"2026-08-16T07:54:21.310Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}