denoland/deno · error · Deno.errors.Http

Already closed

Error message

Already closed

What it means

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.

Source

Thrown at ext/http/00_serve.ts:233

    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 89f33cbef2)

Solutions

  1. Perform the upgrade synchronously during request handling, before any awaits that can complete the response
  2. Branch on the upgrade header early and return the upgrade response from that branch
  3. Catch Deno.errors.Http for late upgrades and treat it as a lost connection, not a crash

Example fix

// before
async function handler(req, info) {
  await doSlowWork();
  return info.upgradeWebSocket(req).response; // Already closed
}

// after
async function handler(req, info) {
  if (wantsWs(req)) {
    const { socket, response } = info.upgradeWebSocket(req); // upgrade first
    doSlowWork().then(() => socket.send("ready"));
    return response;
  }
  await doSlowWork();
  return new Response("ok");
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const { socket, response } = info.upgradeWebSocket(req);
} catch (err) {
  if (err instanceof Deno.errors.Http && err.message === "Already closed") {
    // request finished before upgrade; nothing to salvage, close quietly
    return new Response("upgrade too late", { status: 400 });
  } else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/a2c00e7212d56467. Report an issue: GitHub.