denoland/deno · error · Http
Already closed
Error message
Already closed
What it means
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.
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
Example fix
// before
Deno.serve(async (req) => {
await doAsyncWork();
if (req.headers.get("upgrade") === "websocket") {
return Deno.upgradeWebSocket(req).response; // too late if work completed the request
}
return new Response("ok");
});
// after
Deno.serve((req) => {
if (req.headers.get("upgrade") === "websocket") {
return Deno.upgradeWebSocket(req).response;
}
return doAsyncWork().then(() => new Response("ok"));
}); Defensive patterns
Strategy: try-catch
Try / catch
try {
const { response } = Deno.upgradeWebSocket(request);
return response;
} catch (err) {
if (err instanceof Deno.errors.Http && err.message === "Already closed") {
// request already finalized; nothing to upgrade — log and drop
return new Response("gone", { status: 421 });
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Already upgraded
- Deno.serve: request.signal aborts on successful responses…
- Invalid Header: 'upgrade' header must contain 'websocket'
- Request closed
- ERR_HTTP_HEADERS_SENT
AI-assisted analysis of denoland/deno@abd22074e4 (2026-08-20).
Data as JSON: /api/errors/a2c00e7212d56467.
Report an issue: GitHub.
Appendix: source
Thrown at ext/http/00_serve.ts:248
this.#external = null;
}
get [_upgraded]() {
return this.#upgraded;
}
_throwIfUpgraded() {
if (this.#upgraded) {
throw new Deno.errors.Http("Already upgraded");
}
}
_wantsUpgrade(upgradeType) {
if (this.#upgraded) {
throw new Deno.errors.Http("Already upgraded");
}
if (this.#external === null) {
throw new Deno.errors.Http("Already closed");
}
if (upgradeType == "upgradeWebSocket") {
const external = this.#external;
this.url();
this.headerList;
this.remoteAddr;
this.close();
this.#upgraded = true;
return op_http_upgrade_websocket_next(external);
}
}
url() {
if (this.#urlValue !== undefined) {View on GitHub (pinned to abd22074e4)