firecrawl/firecrawl · error · ZscalerError

auth

auth

Error message

Zscaler rejected the OAuth client credentials

What it means

When the Zscaler token endpoint responds with HTTP 400 or 401, fetchAccessToken throws ZscalerError of kind 'auth' with the status attached. This specifically means the OAuth client credentials (clientId/clientSecret) were rejected — wrong, expired, or revoked. The body is drained to free the connection.

Source

Thrown at apps/api/src/lib/threat-protection/providers/zscaler/client.ts:189

  try {
    response = await fetch(tokenUrl(credentials), {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: body.toString(),
      signal,
      // undici extension: route through the partner egress proxy.
      dispatcher: getDispatcher(),
    } as RequestInit);
  } catch (error) {
    throw new ZscalerError(
      "api",
      `Failed to reach the Zscaler token endpoint: ${error instanceof Error ? error.message : String(error)}`,
    );
  }

  if (response.status === 400 || response.status === 401) {
    drainBody(response);
    throw new ZscalerError(
      "auth",
      "Zscaler rejected the OAuth client credentials",
      response.status,
    );
  }
  if (response.status === 429) {
    drainBody(response);
    throw new ZscalerError(
      "rate-limit",
      "Zscaler token endpoint rate limit hit",
      response.status,
      retryAfterMs(response),
    );
  }
  if (!response.ok) {
    drainBody(response);
    throw new ZscalerError(
      "api",

View on GitHub (pinned to 656bffcc28)

Solutions

  1. Re-issue or re-copy the clientId and clientSecret from the Zscaler/Zidentity console and save them.
  2. Confirm the credentials match the same vanityDomain/cloud being used.
  3. Use Zscaler's two-active-secrets support: add the new secret first, verify, then remove the old one for zero-downtime rotation.
  4. Trim whitespace from credential values before storing them.

Example fix

// before
// credentials.clientSecret = "  abcdef=="  // leading space

// after
const cleanSecret = credentials.clientSecret.trim();
const token = await fetchAccessToken({ ...credentials, clientSecret: cleanSecret }, signal);
Defensive patterns

Strategy: try-catch

Validate before calling

function looksLikeValidZscalerCreds(c: ZscalerCredentials): boolean {
  return !!c.clientId && c.clientId.trim() === c.clientId
    && !!c.clientSecret && c.clientSecret.trim() === c.clientSecret
    && c.clientSecret.length >= 8;
}

if (!looksLikeValidZscalerCreds(credentials)) {
  throw new Error("Zscaler credentials look malformed or contain whitespace");
}

Type guard

function isZscalerAuthError(e: unknown): e is ZscalerError {
  return e instanceof ZscalerError && e.kind === "auth";
}

Try / catch

try {
  token = await fetchAccessToken(credentials, signal);
} catch (e) {
  if (e instanceof ZscalerError && e.kind === "auth") {
    // do NOT retry with the same credentials — they are wrong/revoked
    throw new Error("Zscaler rejected the OAuth client credentials. Re-issue and update them.");
  }
  throw e;
}

Prevention

When it happens

Trigger: The stored ZscalerCredentials have an incorrect clientId or clientSecret, the secret was rotated/revoked in Zscaler, the client is disabled, or the credentials were copied with whitespace/typos. Any 400/401 from the token endpoint maps to this auth error.

Common situations: Secret rotation done in Zscaler but not yet saved in this system; copy-paste error introducing whitespace; wrong Zidentity/vanity cloud; client disabled by a Zscaler admin.

Related errors


AI-assisted analysis of firecrawl/firecrawl@656bffcc28 (2026-08-12). Data as JSON: /api/errors/5c09452bbf59a3bf. Report an issue: GitHub.