windmill-labs/windmill · error · AuthGatewayChallengeError

Got an HTML response from ${url} (status ${status}${cfPart ?

Error message

Got an HTML response from ${url} (status ${status}${cfPart ? `, ${cfPart}` : ""}). The request was intercepted by an upstream auth gateway (likely Cloudflare Access) before reaching Windmill. Verify the runner is on the right network or pass service-token headers via the HEADERS env var (e.g. HEADERS="CF-Access-Client-Id: <id>, CF-Access-Client-Secret: <secret>"). Body starts with: ${JSON.stringify(bodySnippet.slice(0, 120))}

What it means

detectAuthGatewayChallenge in cli/src/utils/http_guards.ts throws AuthGatewayChallengeError when an HTTP response from the Windmill instance looks like HTML served by an upstream auth gateway (Cloudflare Access, SSO wall) instead of the expected JSON API response. The CLI checks the content-type, the cf-mitigated header, and a 256-byte body snippet for a Cloudflare Access sign-in page or any HTML doctype, then aborts so the HTML body is never misparsed as a typed Windmill response. The error carries url, cf-ray, cf-mitigated, status, and a body snippet for diagnosis.

Source

Thrown at cli/src/utils/http_guards.ts:59

  // Cheap check first; only peek the body when something already smells off.
  if (!looksHtml && cfMitigated !== "challenge") return;

  let snippet = "";
  try {
    snippet = (await response.clone().text()).slice(0, 256);
  } catch {
    /* body unreadable — fall through */
  }

  const isChallenge =
    cfMitigated === "challenge" ||
    ACCESS_TITLE.test(snippet) ||
    (looksHtml && HTML_DOCTYPE.test(snippet));

  if (!isChallenge) return;

  throw new AuthGatewayChallengeError(
    url || response.url || "(unknown)",
    response.headers.get("cf-ray") ?? undefined,
    cfMitigated,
    response.status,
    snippet,
  );
}

View on GitHub (pinned to e474e8803c)

Solutions

  1. Set service-token headers via the env var, e.g. HEADERS="CF-Access-Client-Id: <id>, CF-Access-Client-Secret: <secret>" before running the CLI
  2. Run the CLI from a network position that reaches Windmill directly (VPN, allowlisted runner) bypassing the Access challenge
  3. Point BASE_URL/remote at an internal URL that is not behind the auth gateway
  4. Inspect the cf-ray/cf-mitigated values in the error and check your Cloudflare Access logs for the rejected request
  5. If behind a custom gateway, ensure it forwards requests rather than serving its HTML login page to non-browser clients

Example fix

// before: CLI run without Access credentials
export BASE_URL=https://windmill.example.com
wmill sync push
// AuthGatewayChallengeError: Got an HTML response ...

// after: supply Cloudflare Access service tokens
export BASE_URL=https://windmill.example.com
export HEADERS="CF-Access-Client-Id: myapp.access, CF-Access-Client-Secret: ********"
wmill sync push
Defensive patterns

Strategy: try-catch

Validate before calling

const ct = (await fetch(url, { method: "HEAD" })).headers.get("content-type") ?? "";
if (ct.includes("text/html")) {
  throw new Error(`${url} serves HTML — an auth gateway is intercepting; configure HEADERS service tokens before using the CLI`);
}

Type guard

function isAuthGatewayChallenge(err: unknown): err is import("./http_guards.ts").AuthGatewayChallengeError {
  return err instanceof Error && (err as any).name === "AuthGatewayChallengeError" && typeof (err as any).cfRay === "object";
}

Try / catch

import { AuthGatewayChallengeError } from "./utils/http_guards.ts";
try {
  await wmill.createScript(payload);
} catch (e) {
  if (e instanceof AuthGatewayChallengeError) {
    console.error(`Gateway at ${e.url} (status ${e.status}, cf-ray=${e.cfRay}) blocked the request. Set HEADERS with CF-Access service tokens.`);
    process.exitCode = 2;
  } else throw e;
}

Prevention

When it happens

Trigger: Any CLI API call routed through detectAuthGatewayChallenge (generateInlineScriptLock, streamJobWithSSE, docs, updateFlow, createScript, preview) against a Windmill instance behind Cloudflare Access when the request lacks valid service-token headers: response content-type is text/html and body starts with <!doctype or <html, or cf-mitigated: challenge header present, or the body matches the 'Sign in ... Cloudflare Access' title.

Common situations: Running wmill sync/flow dev against a Cloudflare-Access-protected instance from a machine/network outside the allowlist; a corporate proxy or WAF intercepting requests; HEADERS env var missing or malformed (no CF-Access-Client-Id/Secret); pointing the CLI at the public domain instead of an internal address; token expired so Access falls back to the browser login page.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/c440e56b7148f2f1. Report an issue: GitHub.