Skip to content

feat(scrape): optional best-effort MHTML archive of the page - #125

Open
emandel2630 wants to merge 1 commit into
germondai:devfrom
emandel2630:feat/mhtml-archive
Open

feat(scrape): optional best-effort MHTML archive of the page#125
emandel2630 wants to merge 1 commit into
germondai:devfrom
emandel2630:feat/mhtml-archive

Conversation

@emandel2630

Copy link
Copy Markdown
Contributor

Summary

mhtml: true on POST /scrape returns a multipart/related archive of the page in ScrapeResult.mhtml: the rendered DOM first, then the stylesheets, scripts, images and fonts the browser tiers observed loading, each part with its own Content-Location. Text parts are quoted-printable, binary parts base64, 76-column wrapped, CRLF — pure 7-bit ASCII that can be written straight to a .mhtml file.

It is an assembled approximation, not an engine snapshot, and the docs say so. Firefox has no equivalent of Chromium's Page.captureSnapshot, so the archive is built from what the response listener saw: a resource served from the browser's own cache, fetched before the listener attached, or refused on read is simply absent, and document URLs are not rewritten. A reader that resolves subresources by Content-Location (what browsers do with a saved MHTML) gets a usable page.

Design

  • Off by default. Without the flag no subresource body is read and nothing is attached beyond the listeners already there.

  • Rides on fix(scrape): integrate response capture safely #109's response listener rather than opening a second path; unlike pattern capture it does not extend the page's lifetime.

  • Bounded, all parsed with the same captureLimit as CAPTURE_*:

    Variable Default Purpose
    MHTML_MAX_PARTS 200 subresources archived per page
    MHTML_MAX_PART_BYTES 2097152 bytes per subresource
    MHTML_MAX_TOTAL_CHARS 8388608 encoded chars across the archive
    MHTML_MAX_INFLIGHT_READS 32 bodies read at the same time
    MHTML_MAX_OMISSION_RECORDS 100 omissions listed by URL

    A part over budget is dropped whole rather than trimmed (a truncated stylesheet or image is corrupt, not partial). A declared Content-Length past the cap is refused before the body is read; an undeclared one is bounded by the in-flight read count and the post-read cap. Every omission is counted in an X-Trawl-Omitted-Resources header and listed in a final text/plain part, so an archive that hit a cap is still valid MHTML that says what it is missing.

  • Assembly failures leave mhtml unset and never fail the scrape.

Verification

bun run check, bun run typecheck clean; bun test 399 pass / 0 fail on top of dev (c81d628). packages/tiers/tests/mhtml.test.ts covers encoding, resource-type filtering, every bound and omission reason, in-flight limits, unusable Content-Length, unreadable bodies, and tier 2 returning an archive only when asked.

Independent of the blocked-evidence PR; both touch packages/types/src/index.ts and capture.ts in disjoint hunks.

`mhtml: true` on POST /scrape returns a multipart/related archive in
`ScrapeResult.mhtml`: the rendered DOM first, then the stylesheets, scripts,
images and fonts the browser tiers observed loading, each part carrying its own
`Content-Location` so a reader can resolve it back to its URL. Text parts are
quoted-printable over the raw bytes, binary parts base64, both wrapped at 76
characters, so the archive is pure 7-bit ASCII with CRLF endings and can be
written straight to a .mhtml file.

This is an assembled approximation, not an engine snapshot. Firefox exposes no
equivalent of Chromium CDP's Page.captureSnapshot, so the archive is built from
what the response listener saw: a resource served from the browser's own cache,
fetched before the listener attached, or refused on read is simply absent, and
nothing rewrites the document's URLs to point at the archived parts. A reader
that resolves subresources by Content-Location (which is what browsers do with
a saved MHTML) gets a usable page; a byte-faithful reproduction it is not.

Off by default. Without the flag no subresource body is read and no listener is
attached beyond the ones already there - a stock request is unchanged. The
collection rides on the response listener germondai#109 added rather than opening a
second path, and unlike pattern capture it does not extend the page's
lifetime: the archive takes what the page produced during its normal load.

Bounds, all env-tunable and parsed with the same captureLimit as CAPTURE_*:

  MHTML_MAX_PARTS             200      subresources archived per page
  MHTML_MAX_PART_BYTES        2097152  bytes per subresource
  MHTML_MAX_TOTAL_CHARS       8388608  encoded chars across the archive
  MHTML_MAX_INFLIGHT_READS    32       bodies read at the same time
  MHTML_MAX_OMISSION_RECORDS  100      omissions listed by URL

A part over its budget is dropped whole rather than trimmed - a truncated
stylesheet or image is corrupt, not partial. A response is refused on its
declared Content-Length before its body is ever read, so a burst of large
subresources costs nothing to reject; an undeclared length is bounded by the
in-flight read count and the post-read part cap. Every omission is counted in
an `X-Trawl-Omitted-Resources` header and listed in a final text/plain part,
so an archive that hits a cap is still a valid MHTML that says what it is
missing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant