Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/core/batch-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { readAccount } from "./agent-account.ts";
import { registerSecret } from "./logger.ts";
import { CLI_USER_AGENT } from "./config.ts";
import { agentClientHeader } from "./agent-client.ts";
import { BILLING_URL } from "./open-url.ts";

/** Confirmed Batch API base (no trailing slash). */
export const DEFAULT_BATCH_API_BASE = "https://async.api.zenrows.com/v1";
Expand Down Expand Up @@ -176,6 +177,17 @@ function problemToError(status: number, body: string, method: string, path: stri
suggested_commands: ["zenrows batch estimate jobs.jsonl"],
});
}
if (status === 401 && serverCode === "subscription_inactive") {
// The key is fine; the subscription needs a billing action. Same
// AUTH_INVALID code as any 401 so callers that branch on it keep
// working; only the guidance changes, since re-entering the key won't help.
return new ToolkitError({
code: "AUTH_INVALID",
message: "Zenrows refused the Batch API request because the subscription is not active.",
likely_cause: `${cause.replace(/\.?$/, ".")} The API key is valid, but the subscription is past due, unpaid, incomplete or paused.`,
next_action: `Fix the subscription in Billing (${BILLING_URL}), or in Azure Marketplace if you subscribed there, then retry.`,
});
}
if (status === 401) {
return new ToolkitError({
code: "AUTH_INVALID",
Expand Down
1 change: 1 addition & 0 deletions src/core/open-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const DASHBOARD_URL = "https://app.zenrows.com/dashboard";
/** Lands on Billing with the credit-pack purchase modal already open (app PR #1972). */
export const BILLING_TOPUP_URL = "https://app.zenrows.com/billing?topup=open";
export const PLANS_URL = "https://app.zenrows.com/plans";
export const BILLING_URL = "https://app.zenrows.com/billing";
export const DOCS_URL = "https://docs.zenrows.com";

/** Append query params to a URL (used to attach anonymous `utm_*` attribution). */
Expand Down
39 changes: 39 additions & 0 deletions tests/batch-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,45 @@ test("problem+json 402 api_key_cap_reached maps to KEY_CREDIT_CAP_REACHED", asyn
);
});

test("problem+json 401 unauthenticated keeps AUTH_INVALID and the re-login guidance", async () => {
const { impl } = jsonFetch(
401,
{ title: "Unauthenticated (unauthenticated)", status: 401, code: "unauthenticated", detail: "Invalid API key." },
"application/problem+json",
);
await assert.rejects(
() => getJob("job_1", { apiKey: "k", fetchImpl: impl }),
(e: unknown) =>
e instanceof ToolkitError &&
e.code === "AUTH_INVALID" &&
/Re-check your key/.test(e.next_action) &&
(e.suggested_commands ?? []).some((c) => c.startsWith("zenrows login")),
);
});

test("problem+json 401 subscription_inactive keeps AUTH_INVALID but points to Billing", async () => {
const { impl } = jsonFetch(
401,
{
title: "Subscription not active (subscription_inactive)",
status: 401,
code: "subscription_inactive",
detail: "Subscription is not active.",
},
"application/problem+json",
);
await assert.rejects(
() => getJob("job_1", { apiKey: "k", fetchImpl: impl }),
(e: unknown) =>
e instanceof ToolkitError &&
e.code === "AUTH_INVALID" &&
/subscription is not active/.test(e.message) &&
/app\.zenrows\.com\/billing/.test(e.next_action) &&
/Subscription is not active\./.test(e.likely_cause) &&
!(e.suggested_commands ?? []).some((c) => c.startsWith("zenrows login")),
);
});

test("problem+json 404 maps to BATCH_NOT_FOUND", async () => {
const { impl } = jsonFetch(404, { code: "not_found", detail: "job missing" }, "application/problem+json");
await assert.rejects(
Expand Down
Loading