{"record":{"id":"a2c00e7212d56467","repo":"denoland/deno","slug":"already-closed","errorCode":null,"errorMessage":"Already closed","messagePattern":"Already closed","errorType":"exception","errorClass":"Http","httpStatus":null,"severity":"error","filePath":"ext/http/00_serve.ts","lineNumber":248,"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":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/denoland/deno/blob/abd22074e47c6a5cd14e9e4e84743f084aa5a575/ext/http/00_serve.ts#L230-L266","documentation":"Besides the once-only rule, _wantsUpgrade requires the native request handle (#external) to still be alive. Finalizing a request (sending a response, client abort, or close()) sets #external to null. Attempting an upgrade afterwards throws Deno.errors.Http ('Already closed') — the moment to upgrade has passed with the response.","triggerScenarios":"Calling Deno.upgradeWebSocket(request) or upgradeHttp() after the handler already returned a Response; upgrading after an await during which the response completed; upgrade attempts inside cleanup/abort paths that run post-response.","commonSituations":"Returning a normal Response on one path and upgrading on another, with both reached; long-running async work between request receipt and the upgrade decision; framework code that responds first and upgrades later.","solutions":["Decide upgrade-or-respond before any await: upgrade synchronously at handler entry when headers demand it","When upgrading, return the response/object the upgrade API gives you instead of building a normal Response","Do not perform upgrades in background tasks or AbortSignal handlers that can run after completion"],"exampleFix":"// before\nDeno.serve(async (req) => {\n  await doAsyncWork();\n  if (req.headers.get(\"upgrade\") === \"websocket\") {\n    return Deno.upgradeWebSocket(req).response; // too late if work completed the request\n  }\n  return new Response(\"ok\");\n});\n\n// after\nDeno.serve((req) => {\n  if (req.headers.get(\"upgrade\") === \"websocket\") {\n    return Deno.upgradeWebSocket(req).response;\n  }\n  return doAsyncWork().then(() => new Response(\"ok\"));\n});","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const { response } = Deno.upgradeWebSocket(request);\n  return response;\n} catch (err) {\n  if (err instanceof Deno.errors.Http && err.message === \"Already closed\") {\n    // request already finalized; nothing to upgrade — log and drop\n    return new Response(\"gone\", { status: 421 });\n  }\n  throw err;\n}","preventionTips":["Upgrade synchronously at handler entry, before any await","Never return a normal Response on a path that may later upgrade","Keep upgrade logic out of abort/cleanup handlers that run after completion"],"tags":["http","serve","upgrade","websocket","lifecycle"],"backgroundTag":"connection-already-closed","analyzedSha":"abd22074e47c6a5cd14e9e4e84743f084aa5a575","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}