SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
15.9 KB · 286 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import { CodeBlock } from "@/components/ui/code-block";3import { DocPage } from "@/components/docs/doc-page";4import { A, Code, H2, H3, Li, P, Strong, Table, TBody, Td, Th, THead, Tr, Ul } from "@/components/docs/prose";5import { Callout, ComingSoon } from "@/components/docs/callout";6import { Endpoint } from "@/components/docs/endpoint";7import { ParamTable } from "@/components/docs/param-table";8import { CodeTabs } from "@/components/docs/code-tabs";9import { ResponseExample } from "@/components/docs/response-example";10import { fetchTabs } from "@/components/docs/snippets";1112export const metadata: Metadata = {13  title: "Browser",14  description: "Managed browser rendering on POST /v1/fetch: browser, browser_fallback, wait_for, wait_ms, wait_until, javascript, block_resources and screenshot; how automatic escalation works, what is captured, limits and errors.",15};1617const RENDERED = {18  request_id: "req_5d6e7f8g9h0i1j2k",19  success: true,20  status: 200,21  url: "https://app.example.com/dashboard",22  final_url: "https://app.example.com/dashboard?tab=results",23  content: null,24  content_type: "text/html; charset=utf-8",25  headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },26  cookies: [27    { name: "session", value: "9f3a…", domain: "app.example.com", path: "/" },28    { name: "cf_clearance", value: "…", domain: ".example.com", path: "/" },29  ],30  text: "Results\nRef\tPrice\nA-1041\tCA$ 1,250\n…",31  page: { title: "Dashboard — Results", description: null, canonical: "https://app.example.com/dashboard", lang: "en", og: {}, links_count: 64 },32  screenshot: "iVBORw0KGgoAAAANSUhEUgAA…",33  metadata: {34    network: "residential",35    country: "CA",36    mode: "browser",37    attempts: 1,38    duration_ms: 4930,39    bytes: 612340,40    session: "sess_8f2k1m9d3p7q4r6s",41    cached: false,42    timing: { dns_ms: 17, proxy_connect_ms: 0, tls_ms: 0, origin_ms: 4480, processing_ms: 61, total_ms: 4930 },43  },44};4546const ACTIONS: Array<[string, string, string]> = [47  ["goto", "{ url }", "Navigate to a URL and wait for the load event."],48  ["click", "{ selector }", "Click the first element matching a CSS selector."],49  ["type", "{ selector, text, delay? }", "Focus an element and type text, optionally with a per-key delay."],50  ["scroll", "{ selector? | y? }", "Scroll an element into view, or scroll the page by a number of pixels."],51  ["wait", "{ selector? | ms? }", "Wait for a selector to appear, or for a fixed duration."],52  ["evaluate", "{ script }", "Run JavaScript in the page and capture its return value."],53  ["screenshot", "{ full_page?, selector? }", "Capture a PNG of the viewport, the full page or one element."],54  ["extract", "{ schema }", "Return structured data from the rendered DOM (shares the Extraction schema format)."],55];5657export default function BrowserPage() {58  return (59    <DocPage path="/docs/browser" eyebrow="Core API" title="Browser" description="Render JavaScript-heavy pages in a managed headless Chromium that inherits Fetcha's routing, geography and sessions. Available on every fetch with browser: true, and used automatically when a plain request is blocked by a JavaScript challenge." status="Stable">60      <Endpoint method="POST" path="/v1/fetch  { browser: true }" scope="fetch:execute" status="Live" />61      <P>62        Browser rendering is part of the fetch request, not a separate endpoint. The browser runs on Fetcha&apos;s infrastructure, connects through the <Strong>same proxy network, country and63        session</Strong> as a plain fetch, loads the page, waits for it to settle and returns the rendered DOM in the usual response document. No <Code>browser:use</Code> scope is needed (the scope64        name still exists for compatibility).65      </P>6667      <H2>Rendered fetch</H2>68      <ParamTable69        rows={[70          { name: "browser", type: "boolean", default: "false", description: <>Render the page in the managed browser instead of fetching it over plain HTTP.</> },71          { name: "browser_fallback", type: "boolean", default: "true", description: <>Escalate to the browser automatically when an HTTP attempt is blocked by a JavaScript challenge or anti-bot page. Set <code>false</code> to never render.</> },72          { name: "wait_until", type: '"load" | "domcontentloaded" | "networkidle"', default: '"domcontentloaded"', description: <>Navigation event to wait for. <code>networkidle</code> waits until no network request has been made for 500 ms; use it for pages that fetch their data after load.</> },73          { name: "wait_for", type: "string (CSS selector)", constraints: "≤ 512 chars", description: <>Selector that must be present before capture, after <code>wait_until</code>. Fails with <code>BROWSER_TIMEOUT</code> if it never appears within the timeout.</> },74          { name: "wait_ms", type: "integer (ms)", constraints: "0–30,000", description: <>Extra settle time after the wait condition and selector. Useful for pages that animate content in.</> },75          { name: "javascript", type: "boolean", default: "true", description: <>Set <code>false</code> to render with scripting disabled (useful to bypass client-side redirects or paywalls that rely on JS).</> },76          { name: "block_resources", type: "boolean", default: "true", description: <>Skip images, fonts and media. Saves bandwidth and time; scripts, stylesheets and XHR still load.</> },77          { name: "screenshot", type: "boolean", default: "false", description: <>Return a PNG of the viewport in <code>screenshot</code> (base64).</> },78          { name: "device", type: '"desktop" | "mobile" | "tablet"', description: <><code>mobile</code> renders with an iPhone viewport and User-Agent; <code>desktop</code> (default) uses 1366 × 768.</> },79          { name: "locale", type: "string", description: <>Sets the browser language and <code>Accept-Language</code>, e.g. <code>{`"fr-CA"`}</code>.</> },80        ]}81      />82      <P>83        All other fetch fields apply unchanged: <Code>country</Code>, <Code>region</Code>, <Code>city</Code>, <Code>network</Code>, <Code>session</Code>, <Code>headers</Code>, <Code>cookies</Code>,{" "}84        <Code>format</Code>, <Code>links</Code>, <Code>timeout</Code>, <Code>retries</Code> and <Code>debug</Code>. <Code>method</Code> and <Code>body</Code> are ignored in the browser; navigation is always a{" "}85        <Code>GET</Code>.86      </P>8788      <H3>Example</H3>89      <P>Render a dashboard behind a login, reusing the sticky session that holds the login cookies, wait for the results table, and return readable text plus a screenshot.</P>90      <CodeTabs91        tabs={fetchTabs(92          { url: "https://app.example.com/dashboard", browser: true, wait_for: "table.results", wait_ms: 500, country: "CA", session: "sess_8f2k1m9d3p7q4r6s", format: "text", screenshot: true },93          {94            javascript: `console.log(data.metadata.mode, data.page.title); // "browser" "Dashboard — Results"\nawait fs.promises.writeFile("dashboard.png", Buffer.from(data.screenshot, "base64"));`,95            python: `print(data["metadata"]["mode"], data["page"]["title"])\nopen("dashboard.png", "wb").write(base64.b64decode(data["screenshot"]))`,96          },97        )}98      />99      <ResponseExample status={200} title="200 OK · metadata.mode: browser" body={RENDERED} />100101      <H2>What is captured</H2>102      <Ul>103        <Li>104          <Strong>DOM after settle.</Strong> <Code>content</Code> is the serialised document (<Code>document.documentElement.outerHTML</Code>) once <Code>wait_until</Code>, <Code>wait_for</Code> and{" "}105          <Code>wait_ms</Code> are satisfied. <Code>format: text</Code> and <Code>markdown</Code> convert this rendered DOM, so client-side content is included.106        </Li>107        <Li>108          <Strong>Final URL.</Strong> <Code>final_url</Code> reflects server redirects and client-side navigations (<Code>location.replace</Code>, meta refresh, framework routers) that happened before capture.109        </Li>110        <Li>111          <Strong>Status and headers of the main document.</Strong> <Code>status</Code> and <Code>headers</Code> come from the main navigation response, not from sub-resources. A page that loads but shows an112          error inside the app still reports the document&apos;s status.113        </Li>114        <Li>115          <Strong>Cookies.</Strong> <Code>cookies</Code> contains the browser&apos;s cookie jar for the site after rendering, including cookies set by JavaScript and challenge clearances. Replay them via the{" "}116          <Code>cookies</Code> field or keep the <Code>session</Code>.117        </Li>118        <Li>119          <Strong>Page metadata and links.</Strong> <Code>page</Code> (title, description, canonical, lang, Open Graph, link count) and, with <Code>links: true</Code>, <Code>links[]</Code> are extracted from the120          rendered DOM.121        </Li>122        <Li>123          <Strong>Optional screenshot.</Strong> A PNG of the viewport (1366 × 768 desktop, 390 × 844 mobile), base64-encoded in <Code>screenshot</Code>.124        </Li>125      </Ul>126127      <H2>Captcha solving</H2>128      <P>129        Most JavaScript challenges clear on their own inside the real browser. When Cloudflare asks for an interactive Turnstile verification instead, Fetcha intercepts the widget parameters,130        obtains a token from a managed human-verification service (2captcha) and hands it to the page, adopting the user agent the token was issued for. This typically adds 10–40 seconds, so keep131        <Code>timeout</Code> generous (60 s or more) for hard targets. Set <Code>{`solve_captcha: false`}</Code> to opt out on a request. Attempts that needed a token report{" "}132        <Code>captcha_solved: true</Code> in <Code>metadata.debug.attempts[]</Code>.133      </P>134135      <H2>Automatic escalation</H2>136      <P>137        You rarely need to set <Code>browser: true</Code> yourself. With the default <Code>browser_fallback: true</Code>, Fetcha starts every request over plain HTTP because it is faster and cheaper.138        When an attempt is classified as a JavaScript challenge or an anti-bot interstitial (Cloudflare challenge or Turnstile, DataDome, PerimeterX, Akamai, Kasada, Imperva, AWS WAF, Vercel attack139        mode, or a soft 200 challenge page), the router escalates: it re-plays the request in the browser through the same route, letting the challenge script run and the clearance cookie be set.140      </P>141      <Ul>142        <Li>143          Escalation counts as one attempt and shares the request&apos;s single <Code>timeout</Code> and <Code>retries</Code> budget.144        </Li>145        <Li>146          <Code>metadata.mode</Code> is <Code>browser</Code> when the final attempt was rendered. With <Code>debug: true</Code>, each attempt lists its <Code>mode</Code> and, for blocked ones, the{" "}147          <Code>block_reason</Code>.148        </Li>149        <Li>Plain 403/429 blocks without a JavaScript challenge are retried on a new IP or a premium route first; the browser is used when the block needs a script to be executed.</Li>150        <Li>151          Set <Code>browser_fallback: false</Code> for latency-sensitive calls where a blocked answer is acceptable, or when you handle challenges yourself.152        </Li>153      </Ul>154      <Callout variant="info" title="Learning">155        Escalations feed the per-domain profile like any other attempt. A site that consistently needs the browser will be rendered directly on later requests, saving the wasted HTTP attempt.156      </Callout>157158      <H2>Limits and errors</H2>159      <Table dense>160        <THead>161          <Tr>162            <Th>Limit</Th>163            <Th>Value</Th>164          </Tr>165        </THead>166        <TBody>167          <Tr>168            <Td>Concurrent renders per organization</Td>169            <Td mono>8</Td>170          </Tr>171          <Tr>172            <Td>Render time</Td>173            <Td>174              Bounded by the request <Code>timeout</Code> (max 120 s), shared with any HTTP attempts made before escalation175            </Td>176          </Tr>177          <Tr>178            <Td>Viewport</Td>179            <Td mono>1366 × 768 desktop · 390 × 844 mobile</Td>180          </Tr>181          <Tr>182            <Td>Response size</Td>183            <Td>Same 20 MB cap as plain fetches, applied to the serialised DOM. Screenshots are not counted.</Td>184          </Tr>185        </TBody>186      </Table>187      <Table dense>188        <THead>189          <Tr>190            <Th>Situation</Th>191            <Th>Result</Th>192          </Tr>193        </THead>194        <TBody>195          <Tr>196            <Td>197              Page did not reach <Code>wait_until</Code>, <Code>wait_for</Code> never appeared, or the render exceeded the remaining <Code>timeout</Code>198            </Td>199            <Td mono>504 BROWSER_TIMEOUT</Td>200          </Tr>201          <Tr>202            <Td>Browser pool disabled or unavailable</Td>203            <Td mono>400 BROWSER_UNAVAILABLE</Td>204          </Tr>205          <Tr>206            <Td>More than 8 renders in flight for the organization</Td>207            <Td mono>429 CONCURRENCY_LIMIT</Td>208          </Tr>209          <Tr>210            <Td>Target blocked even in the browser, on every attempt</Td>211            <Td>212              <Code>200</Code> with <Code>success: false</Code> and the last page, like any block213            </Td>214          </Tr>215        </TBody>216      </Table>217      <P>218        Rendered requests are logged like any other request: the request log shows <Strong>mode</Strong> per attempt, and bandwidth is metered on the bytes the browser actually transferred (which is219        why <Code>block_resources</Code> is on by default).220      </P>221222      <H2>Tips</H2>223      <Ul>224        <Li>225          Prefer <Code>wait_for</Code> over a large <Code>wait_ms</Code>: it returns as soon as the content exists and fails clearly when it never does.226        </Li>227        <Li>228          Use <Code>{`format: "markdown"`}</Code> or <Code>{`"text"`}</Code> with the browser to get the rendered content without shipping the framework&apos;s HTML.229        </Li>230        <Li>231          Combine with a <A href="/docs/sessions">session</A> for logged-in areas: the browser reuses the session&apos;s IP and the cookies you pass, and returns the updated jar.232        </Li>233        <Li>234          If the site exposes the JSON endpoint the page calls, fetching it directly with <Code>{`format: "json"`}</Code> is still faster than rendering. See <A href="/docs/examples">Examples</A>.235        </Li>236      </Ul>237238      <H2>Coming soon: browser actions</H2>239      <ComingSoon title="POST /v1/browser is not yet available">240        Multi-step interactions (click, type, scroll, evaluate) in one browser context are planned. <Code>POST /v1/browser</Code> currently returns <Code>400 BROWSER_UNAVAILABLE</Code>. The design below241        is indicative; field names may change before launch. Follow the <A href="/changelog">changelog</A>.242      </ComingSoon>243      <Endpoint method="POST" path="/v1/browser" status="Coming soon" />244      <Table>245        <THead>246          <Tr>247            <Th>Action</Th>248            <Th>Parameters</Th>249            <Th>Description</Th>250          </Tr>251        </THead>252        <TBody>253          {ACTIONS.map(([name, params, desc]) => (254            <Tr key={name}>255              <Td mono>{name}</Td>256              <Td mono>{params}</Td>257              <Td>{desc}</Td>258            </Tr>259          ))}260        </TBody>261      </Table>262      <CodeBlock263        lang="json"264        title="Planned request"265        code={JSON.stringify(266          {267            url: "https://www.example.ca/login",268            country: "CA",269            session: "sess_8f2k1m9d3p7q4r6s",270            actions: [271              { type: "type", selector: "#email", text: "me@example.com" },272              { type: "type", selector: "#password", text: "•••••••" },273              { type: "click", selector: "button[type=submit]" },274              { type: "wait", selector: ".account-summary" },275              { type: "screenshot", full_page: false },276              { type: "extract", schema: { balance: "string", plan: "string" } },277            ],278          },279          null,280          2,281        )}282      />283    </DocPage>284  );285}286