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

  1. Decide upgrade-or-respond before any await: upgrade synchronously at handler entry when headers demand it
  2. When upgrading, return the response/object the upgrade API gives you instead of building a normal Response
  3. 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

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


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)