denoland/deno · error · Deno.errors.Http

Already upgraded

Error message

Already upgraded

What it means

Once a request handled by Deno.serve has been upgraded (e.g. to a WebSocket), its request context is finalized and cannot produce a normal HTTP response. The _throwIfUpgraded guard rejects response-related operations with a Deno.errors.Http error. Only one terminal action — upgrade or respond — is allowed per request.

Source

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

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

Solutions

  1. Return immediately after performing the upgrade; make upgrade branches exclusive
  2. Restructure the handler so upgrade and respond paths are early-returning if/else branches
  3. Catch Deno.errors.Http in middleware that may run after an upgrade and treat it as end-of-processing

Example fix

// before
const { socket, response } = info.upgradeWebSocket(req);
return new Response("upgraded"); // Deno.errors.Http: Already upgraded

// after
if (req.headers.get("upgrade") === "websocket") {
  return info.upgradeWebSocket(req).response; // exclusive branch
}
return new Response("normal");
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await respond(ctx, response);
} catch (err) {
  if (err instanceof Deno.errors.Http && err.message === "Already upgraded") {
    // request was upgraded; skip response handling silently
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the response path (e.g. returning/committing a Response via APIs that route through _throwIfUpgraded) after a successful upgradeWebSocket on the same request within a serve handler.

Common situations: Handlers that upgrade and then fall through to a return statement with a Response; middleware that writes a response after an inner handler already upgraded; retry/fallback logic that tries to respond normally after a failed upgrade succeeded partially.

Related errors


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