Field guide + executed synthetic lab

One URL, three artifacts

“Get this page” is not a technical contract. A server response, a rendered browser state, and a structured record answer different questions. Keep them separate and the result becomes easier to reproduce, debug, and review.

This guide includes a deterministic synthetic fixture and a zero-dependency Node lab. The lab executed local HTTP and extraction checks. Its mutation example uses a small simulated DOM adapter, recorded separately from a later browser final-state check. No vendor extraction tool, hosted API, third-party page, account, or paid service was tested.

1. Fetch: preserve what the server returned

A fetch artifact should retain the requested URL, redirect steps, final URL, target HTTP status, content type, raw bytes, retrieval time, and a body digest. Do not reduce all of that to “the request worked.”

The lab requests /redirect without following it, observes 302 and Location: /fixture, then follows the redirect and observes the fixture’s 200 response. It also requests a controlled missing route and observes 404. The fixture body is hashed with SHA-256 from the exact response bytes.

This distinction also matters behind an extraction service. Firecrawl’s scrape documentation explicitly separates the API request status and success flag from the target page’s data.metadata.statusCode. Preserve both layers when both exist; a successful API call can still report a failed target page.

2. Render: define the state you waited for

Rendering adds an environment and a completion condition. Record the browser engine and version, viewport, navigation strategy, wait condition, DOM or accessibility snapshot, and any interactions. “Loaded” is ambiguous when content appears later.

The synthetic fixture initially contains:

<span id="price" data-delay-ms="25">Loading</span>

Its script changes that text to $24.00 after a declared 25 ms delay. The local lab executes only this known script against a minimal simulated DOM and records:

{
  "adapter": "fixture-simulated-dom/v1",
  "browserEngine": null,
  "initialPrice": "Loading",
  "renderedPrice": "$24.00",
  "declaredDelayMs": 25
}

That proves only that the fixture-specific simulator applied the declared mutation. It does not prove browser layout, JavaScript compatibility, network-idle behavior, screenshots, accessibility trees, or any browser automation product.

Separate browser final-state observation

On September 20, 2026, an automated Playwright inspection opened the loopback fixture at a 390 × 844 viewport. It observed the main heading Fictional Demo Mug, the visible paragraph Price: $24.00, and #price inner text equal to $24.00. The browser console exposed no error or warning messages during that check.

This is a narrow final-state pass. The exact browser engine version was not recorded, and the check did not observe the initial Loading state, measure the 25 ms delay, capture timing, or compare any vendor tool. The Node simulator’s recorded delay and the browser’s final-state observation are separate evidence.

3. Extract: bind output to its input

An extraction record should name the input digest, rule or schema version, output digest, required fields, optional misses, rejected records, and warnings. Otherwise a clean JSON object can hide a stale page, partial parse, or schema mismatch.

The fixture contains a JSON-LD product plus visible main content. The local extractor reads both and produces a normalized record:

{
  "heading": "Fictional Demo Mug",
  "product": {
    "name": "Fictional Demo Mug",
    "sku": "SYN-004",
    "price": "24.00",
    "currency": "USD",
    "availability": null
  },
  "warnings": ["optional field missing: availability"]
}

The missing optional field remains visible instead of being invented. The extractor hashes its normalized JSON output and ties it to the fixture body hash.

Choose the smallest sufficient contract

QuestionMinimum artifactCompletion signal
What bytes and status did the server return?FetchFinal response captured and hashed
What state appeared after client-side behavior?RenderNamed DOM/accessibility condition observed
What fields can a pipeline consume?ExtractSchema evaluated with misses and warnings retained
Did a user-facing action succeed?Render plus action traceObservable postcondition, not merely a click

Use a browser only when browser state or interaction is part of the question. Use extraction when normalized content is the deliverable. Keep the fetch record underneath both when provenance matters.

Failure is part of the record

Classify failures at the layer where they occur:

  • transport or DNS failure;
  • redirect refusal or unexpected final URL;
  • target-page 4xx or 5xx;
  • render timeout or unmet DOM condition;
  • selector, parse, or schema mismatch;
  • partial extraction with missing fields; or
  • ambiguous action completion.

Retain the failed artifact when it is safe to do so. A retry is a new observation with a new retrieval time, not retroactive proof that the first attempt succeeded.

Documented tool contracts, not local results

The Firecrawl scrape documentation documents Markdown, HTML, raw HTML, links, JSON and other output formats, status metadata, and caching controls. Crawl4AI’s simple-crawling guide documents an asynchronous crawler result interface. Browser Use’s tool documentation documents actions exposed to a browser-using agent.

Those interfaces illustrate overlap between extraction and action, but the products were not installed or compared in this lab. No claim is made about their accuracy, speed, cost, JavaScript coverage, authentication behavior, or suitability.

Download and reproduce

Prerequisite: Node.js 24 or newer. The lab uses Node's global fetch API and built-in modules only; it has no third-party package dependencies. The recorded local run used Node.js 24.19.0.

To reproduce outside the repository, download the HTML and Node files into one directory, rename web-acquisition-fixture.mjs.txt to web-acquisition-fixture.mjs, and run node web-acquisition-fixture.mjs. The script prefers the colocated fixture. Run node web-acquisition-fixture.mjs --serve to serve it only on http://127.0.0.1:4179/fixture; the expected post-delay condition is selector #price with text $24.00. Stop it with Ctrl+C.

In the source repository, node scripts/web-acquisition-fixture.mjs runs the same script against the canonical fixture. The test suite asserts the redirect, final 200, controlled 404, exact body digest, simulated mutation, extracted fields, missing-field warning, and exact normalized-output digest. The separate browser evidence above supports only the observed final state.

Continue

Keep reading

Browser interface contract translation →

All library entries →