denoland/deno · error · Deno.errors.Http

Already upgraded

Error message

Already upgraded

What it means

In Deno.serve, an incoming request can be upgraded at most once. _throwIfUpgraded is the guard invoked by the public upgrade helpers (e.g. Deno.upgradeWebSocket) before performing an upgrade; if the internal flag #upgraded is already set, it throws Deno.errors.Http ('Already upgraded'), because the connection has already been converted to another protocol.

Solutions

  1. Upgrade exactly once per request and return the expected handler result immediately after
  2. Branch on request.headers.get('upgrade') first so only one upgrade path can run
  3. Track upgrade state in your own handler scope (e.g. let upgraded = false) around shared helpers

Example fix

// before
const { socket, response } = Deno.upgradeWebSocket(request);
// later, another branch:
Deno.upgradeWebSocket(request); // Deno.errors.Http: Already upgraded

// after
let upgraded = false;
if (!upgraded && request.headers.get("upgrade")?.toLowerCase() === "websocket") {
  upgraded = true;
  const { socket, response } = Deno.upgradeWebSocket(request);
  return response;
}
Defensive patterns

Strategy: try-catch

Try / catch

let upgraded = false;
function upgradeOnce(request) {
  if (upgraded) return null;
  upgraded = true;
  try {
    return Deno.upgradeWebSocket(request);
  } catch (err) {
    if (err instanceof Deno.errors.Http) return null; // Already upgraded/closed
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling Deno.upgradeWebSocket(request) a second time after a successful first upgrade; a handler that upgrades and then another code path (retry wrapper, fallthrough branch) attempts the upgrade again.

Common situations: Handlers with multiple branches that both upgrade; middleware that re-invokes the handler; upgrading both WebSocket and HTTP on the same request; retry loops around the handler call.

Related errors


AI-assisted analysis of denoland/deno@abd22074e4 (2026-08-20). Data as JSON: /api/errors/5cea0416f54b669e. Report an issue: GitHub.

Appendix: source

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

      if (success && this.#signalAccessed && !legacyAbortWarned) {
        legacyAbortWarned = true;
        // deno-lint-ignore no-console
        console.warn(
          "Deno.serve: request.signal aborts on successful responses (legacy behavior). To detect when a request has been fully delivered use the `completed` promise on the handler's info argument. Move cleanup to the handler's return path, or opt in to the new behavior with --unstable-no-legacy-abort. See https://docs.deno.com/go/unstable-no-legacy-abort",
        );
      }
      abortRequest(this.request);
    }
    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();

View on GitHub (pinned to abd22074e4)