TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { Metadata } from "next";2import { ERROR_CODES, ERROR_HTTP_STATUS, ERROR_MESSAGES, type ErrorCode } from "@fetcha/core";3import { CodeBlock } from "@/components/ui/code-block";4import { DocPage } from "@/components/docs/doc-page";5import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";6import { Callout } from "@/components/docs/callout";7import { ParamTable } from "@/components/docs/param-table";8import { ResponseExample } from "@/components/docs/response-example";9import { CodeTabs } from "@/components/docs/code-tabs";1011export const metadata: Metadata = {12 title: "Errors",13 description: "Error envelope, every error code with its HTTP status and meaning, validation details, Retry-After, request IDs and error handling in the SDKs.",14};1516/** Extra guidance per code, beyond the canonical message. */17const NOTES: Record<ErrorCode, { retry: "no" | "after-fix" | "backoff" | "later"; note: string }> = {18 INVALID_API_KEY: { retry: "after-fix", note: "Missing header, malformed token, wrong prefix, revoked or expired key. Check the message for the exact reason." },19 EMAIL_NOT_VERIFIED: { retry: "after-fix", note: "Verify the organization owner's email address in the dashboard, then retry." },20 RATE_LIMITED: { retry: "backoff", note: "A sliding-window limit was hit. Honour the Retry-After header (seconds); details.retry_after_ms has the precise value." },21 CONCURRENCY_LIMIT: { retry: "backoff", note: "Too many in-flight requests for the organization. details.limit is your plan's concurrency. Queue and retry when a request finishes." },22 INVALID_REQUEST: { retry: "after-fix", note: "Schema violation, unknown field, malformed JSON, wrong Content-Type or body over 4 MB. details.issues lists each problem." },23 URL_NOT_ALLOWED: { retry: "no", note: "Non-http scheme, credentials in URL, or a host that is local, private, link-local, metadata or internal — on the initial URL or a redirect hop." },24 TARGET_TIMEOUT: { retry: "later", note: "The request budget (timeout) was exhausted across all attempts. Raise timeout within your plan cap, or retry later." },25 TARGET_BLOCKED: { retry: "later", note: "Recorded in the request log when every attempt was blocked. On the API this surfaces as a 200 with success:false and the last blocked page." },26 TARGET_UNAVAILABLE: { retry: "later", note: "DNS resolution failed, no address records, or the connection was refused on every attempt." },27 PROVIDER_UNAVAILABLE: { retry: "backoff", note: "No route is available for an auto request (all circuits open or nothing configured for the geography). Transient; retry with backoff." },28 NETWORK_UNAVAILABLE: { retry: "after-fix", note: "Explicit class not in your plan, not live yet, or without a sticky-capable route (sessions). Use auto or residential." },29 BROWSER_UNAVAILABLE: { retry: "later", note: "Browser pool disabled or unavailable. Retry later, or send the request without browser: true." },30 BROWSER_TIMEOUT: { retry: "later", note: "The page did not settle in the managed browser within the request timeout. Raise timeout, relax wait_for / wait_until, or retry." },31 RESPONSE_TOO_LARGE: { retry: "after-fix", note: "Body exceeded max_response_bytes or the 20 MB platform cap. Not retried across routes." },32 TOO_MANY_REDIRECTS: { retry: "after-fix", note: "More than max_redirects hops. Not retried across routes." },33 INSUFFICIENT_CREDITS: { retry: "after-fix", note: "Reserved for prepaid balances. Not emitted today." },34 USAGE_LIMIT_REACHED: { retry: "after-fix", note: "Monthly plan quota, project request limit, or a hard spending limit (organization or project). details tells which; see Rate limits." },35 SESSION_NOT_FOUND: { retry: "after-fix", note: "Unknown session id, or a session that belongs to another project." },36 SESSION_EXPIRED: { retry: "after-fix", note: "Session past expires_at or closed. Create a new one." },37 NOT_FOUND: { retry: "no", note: "Unknown route or resource. The message names the route." },38 FORBIDDEN: { retry: "after-fix", note: "Key lacks the required scope, project archived, or organization/account suspended." },39 INTERNAL_ERROR: { retry: "backoff", note: "Unexpected failure inside Fetcha. Retry with backoff; if it persists, send us the request_id." },40 CRAWL_NOT_FOUND: { retry: "after-fix", note: "GET/DELETE /v1/crawl/:id with an id that does not exist or belongs to another project." },41 CRAWL_LIMIT_REACHED: { retry: "later", note: "Too many crawl jobs running for the organization (5 concurrent). Wait for one to finish or cancel it." },42};4344const RETRY_LABEL: Record<(typeof NOTES)[ErrorCode]["retry"], string> = {45 no: "Do not retry",46 "after-fix": "Fix, then retry",47 backoff: "Retry with backoff",48 later: "Retry later",49};5051const GROUPS: Array<{ title: string; codes: ErrorCode[] }> = [52 { title: "Authentication and authorization", codes: ["INVALID_API_KEY", "EMAIL_NOT_VERIFIED", "FORBIDDEN"] },53 { title: "Request validation", codes: ["INVALID_REQUEST", "URL_NOT_ALLOWED", "NETWORK_UNAVAILABLE", "BROWSER_UNAVAILABLE", "NOT_FOUND"] },54 { title: "Limits and quotas", codes: ["RATE_LIMITED", "CONCURRENCY_LIMIT", "USAGE_LIMIT_REACHED", "INSUFFICIENT_CREDITS", "CRAWL_LIMIT_REACHED"] },55 { title: "Target and network", codes: ["TARGET_TIMEOUT", "TARGET_BLOCKED", "TARGET_UNAVAILABLE", "PROVIDER_UNAVAILABLE", "RESPONSE_TOO_LARGE", "TOO_MANY_REDIRECTS", "BROWSER_TIMEOUT"] },56 { title: "Sessions and crawls", codes: ["SESSION_NOT_FOUND", "SESSION_EXPIRED", "CRAWL_NOT_FOUND"] },57 { title: "Server", codes: ["INTERNAL_ERROR"] },58];5960const JS_HANDLING = `import { Fetcha, FetchaError } from "@fetcha/sdk";6162const fetcha = new Fetcha({ apiKey: process.env.FETCHA_API_KEY! });6364try {65 const r = await fetcha.fetch({ url: "https://example.com", country: "CA" });66 if (!r.success) {67 // Target answered but with an error or a block page: r.status, r.metadata.attempts68 console.warn("target status", r.status, "after", r.metadata.attempts, "attempts");69 }70} catch (e) {71 if (e instanceof FetchaError) {72 // e.code, e.status (HTTP), e.message, e.requestId73 switch (e.code) {74 case "RATE_LIMITED":75 case "CONCURRENCY_LIMIT":76 case "PROVIDER_UNAVAILABLE":77 // back off and retry78 break;79 case "INVALID_REQUEST":80 // fix the request; details are in e.message81 break;82 default:83 console.error(\`\${e.code} (\${e.status}) \${e.message} — request \${e.requestId}\`);84 }85 } else {86 throw e; // network failure reaching Fetcha, JSON parse error, abort87 }88}`;8990const PY_HANDLING = `from fetcha import Fetcha, FetchaError9192client = Fetcha(api_key=os.environ["FETCHA_API_KEY"])9394try:95 r = client.fetch("https://example.com", country="CA")96 if not r.success:97 # Target answered but with an error or a block page98 print("target status", r.status, "after", r.metadata["attempts"], "attempts")99except FetchaError as e:100 # e.code, e.status (HTTP), e.message, e.request_id101 if e.code in ("RATE_LIMITED", "CONCURRENCY_LIMIT", "PROVIDER_UNAVAILABLE"):102 ... # back off and retry103 elif e.code == "INVALID_REQUEST":104 ... # fix the request105 else:106 print(f"{e.code} ({e.status}) {e.message} — request {e.request_id}")`;107108export default function ErrorsPage() {109 const all: ErrorCode[] = [...ERROR_CODES];110 return (111 <DocPage path="/docs/errors" eyebrow="Reliability" title="Errors" description="Fetcha uses conventional HTTP status codes and a single JSON envelope for every error. Each error carries a stable machine-readable code and the request ID." status="Stable">112 <H2>Error envelope</H2>113 <P>114 Every non-2xx response from the API has this shape. <Code>code</Code> is stable and meant for programmatic handling; <Code>message</Code> is human-readable and may change;{" "}115 <Code>details</Code> is present only for some codes.116 </P>117 <ResponseExample118 status={429}119 statusText="Too Many Requests"120 body={{ error: { code: "RATE_LIMITED", message: "Too many requests. Slow down or upgrade your plan.", request_id: "req_4e5f6g7h8i9j0k1l", details: { retry_after_ms: 640 } } }}121 />122 <ParamTable123 showDefault={false}124 rows={[125 { name: "error.code", type: "string", description: <>One of the {all.length} codes below.</> },126 { name: "error.message", type: "string", description: <>Explanation for humans. Often more specific than the default message (for example which scope is missing or which plan lacks a network).</> },127 { name: "error.request_id", type: "string | null", description: <>The request identifier, also in the <code>X-Fetcha-Request-ID</code> header.</> },128 { name: "error.details", type: "object", description: <>Optional structured context: <code>issues</code> for validation, <code>retry_after_ms</code> for rate limits, <code>limit</code> for concurrency, <code>limit</code>/<code>used</code> or <code>limit_usd</code>/<code>spent_usd</code> for quotas.</> },129 ]}130 />131 <Callout variant="info" title="A 200 is not always a success">132 <Code>/v1/fetch</Code> returns <Code>200</Code> whenever Fetcha obtained a response from the target, including a 404, a 500 or a block page. Check <Code>success</Code> and <Code>status</Code>{" "}133 in the body. Error envelopes are used only when Fetcha itself could not complete the request. See <A href="/docs/fetch#blocked-targets">Blocked targets</A>.134 </Callout>135136 <H2>Error codes</H2>137 <P>The status, code and default message come straight from the API's error catalogue. The last two columns are guidance.</P>138 {GROUPS.map((g) => (139 <div key={g.title}>140 <H3>{g.title}</H3>141 <Table dense>142 <THead>143 <Tr>144 <Th>Status</Th>145 <Th>Code</Th>146 <Th>Default message and notes</Th>147 <Th>Retry</Th>148 </Tr>149 </THead>150 <TBody>151 {g.codes.map((code) => (152 <Tr key={code}>153 <Td mono className="whitespace-nowrap">154 {ERROR_HTTP_STATUS[code]}155 </Td>156 <Td mono className="whitespace-nowrap">157 {code}158 </Td>159 <Td>160 <div className="text-fg">{ERROR_MESSAGES[code]}</div>161 <div className="mt-0.5 text-[12.5px] text-fg-muted">{NOTES[code].note}</div>162 </Td>163 <Td className="whitespace-nowrap text-[12.5px]">{RETRY_LABEL[NOTES[code].retry]}</Td>164 </Tr>165 ))}166 </TBody>167 </Table>168 </div>169 ))}170171 <H2>Validation details</H2>172 <P>173 <Code>INVALID_REQUEST</Code> raised by schema validation includes <Code>details.issues</Code>, an array with one entry per problem. <Code>path</Code> is the dotted path of the offending field174 (empty for unknown top-level keys), <Code>message</Code> explains the constraint.175 </P>176 <ResponseExample177 status={400}178 statusText="Bad Request"179 body={{180 error: {181 code: "INVALID_REQUEST",182 message: "The request body is invalid.",183 request_id: "req_9c1d2e3f4a5b6c7d",184 details: {185 issues: [186 { path: "url", message: "String must contain at least 1 character(s)" },187 { path: "country", message: "String must contain exactly 2 character(s)" },188 { path: "max_redirects", message: "Number must be less than or equal to 20" },189 { path: "", message: "Unrecognized key(s) in object: 'proxy'" },190 ],191 },192 },193 }}194 />195 <P>Other request-level failures reuse the same code with a specific message and no <Code>issues</Code>:</P>196 <Ul>197 <Li>198 <Code>Malformed JSON body.</Code>199 </Li>200 <Li>201 <Code>Send a JSON body with Content-Type: application/json.</Code>202 </Li>203 <Li>204 <Code>Request body too large.</Code> (HTTP 413)205 </Li>206 </Ul>207208 <H2>Retry-After</H2>209 <P>210 <Code>RATE_LIMITED</Code> responses include a <Code>Retry-After</Code> header in whole seconds (rounded up from <Code>details.retry_after_ms</Code>). Honour it. <Code>CONCURRENCY_LIMIT</Code>{" "}211 does not carry the header because the right moment to retry is when one of your own in-flight requests completes.212 </P>213 <CodeBlock214 lang="text"215 code={`HTTP/1.1 429 Too Many Requests216Retry-After: 1217X-Fetcha-Request-ID: req_4e5f6g7h8i9j0k1l218Content-Type: application/json; charset=utf-8`}219 />220221 <H2>Request IDs and support</H2>222 <P>223 Every response has an <Code>X-Fetcha-Request-ID</Code>; error envelopes repeat it in <Code>error.request_id</Code>. Log it alongside your own correlation id. When you contact{" "}224 <A href="mailto:support@fetcha.co">support@fetcha.co</A>, include the request id, the approximate time (UTC) and the target domain: with the id we can see every attempt, the route used, the225 outcome and the timing, without you sharing any content.226 </P>227 <P>228 <Strong>Retention:</Strong> request metadata is kept for the retention period of your plan (3 to 365 days). Response bodies are not stored, and sensitive request headers are redacted before229 logging.230 </P>231232 <H2>Handling errors in the SDKs</H2>233 <P>234 Both SDKs raise a <Code>FetchaError</Code> for any non-2xx response, exposing <Code>code</Code>, <Code>status</Code>, <Code>message</Code> and the request id. A fetch that reached the target235 never throws: inspect <Code>success</Code> instead. See <A href="/docs/sdks">SDKs</A>.236 </P>237 <CodeTabs238 tabs={[239 { label: "JavaScript", lang: "typescript", code: JS_HANDLING },240 { label: "Python", lang: "python", code: PY_HANDLING },241 ]}242 />243 </DocPage>244 );245}246