feat(scrape): optional best-effort MHTML archive of the page - #125
Open
emandel2630 wants to merge 1 commit into
Open
feat(scrape): optional best-effort MHTML archive of the page#125emandel2630 wants to merge 1 commit into
emandel2630 wants to merge 1 commit into
Conversation
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
mhtml: trueonPOST /scrapereturns amultipart/relatedarchive of the page inScrapeResult.mhtml: the rendered DOM first, then the stylesheets, scripts, images and fonts the browser tiers observed loading, each part with its ownContent-Location. Text parts are quoted-printable, binary parts base64, 76-column wrapped, CRLF — pure 7-bit ASCII that can be written straight to a.mhtmlfile.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 byContent-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
captureLimitasCAPTURE_*:MHTML_MAX_PARTSMHTML_MAX_PART_BYTESMHTML_MAX_TOTAL_CHARSMHTML_MAX_INFLIGHT_READSMHTML_MAX_OMISSION_RECORDSA part over budget is dropped whole rather than trimmed (a truncated stylesheet or image is corrupt, not partial). A declared
Content-Lengthpast 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 anX-Trawl-Omitted-Resourcesheader and listed in a finaltext/plainpart, so an archive that hit a cap is still valid MHTML that says what it is missing.Assembly failures leave
mhtmlunset and never fail the scrape.Verification
bun run check,bun run typecheckclean;bun test399 pass / 0 fail on top ofdev(c81d628).packages/tiers/tests/mhtml.test.tscovers encoding, resource-type filtering, every bound and omission reason, in-flight limits, unusableContent-Length, unreadable bodies, and tier 2 returning an archive only when asked.Independent of the blocked-evidence PR; both touch
packages/types/src/index.tsandcapture.tsin disjoint hunks.