Build1 publisher3 min readPublished
The capture returned HTTP 200. The file was a Cloudflare block page.
A developer's harassment-evidence service kept succeeding after a third-party embed wrapper's domain was retired. The stored artifact was an error screen, and exit status never said so.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- The author of a dev.to post builds a web service that preserves evidence of harassment on social platforms, whose core feature is automatically capturing a real screenshot of the offending post.
- The author built an alternative that pulled the post text through an API and rendered a tidy 'evidence card' image, then threw it away, on the grounds that an image you can author freely afterwards proves nothing.
- The author started with Cloudflare Browser Rendering; the wiring worked but the capture did not.
- X blocks headless browsers and the request times out.
- YouTube refuses script injection under a Trusted Types CSP, so there is no way to make it render the comment.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer who runs a service that preserves screenshots of harassing social media posts discovered his pipeline had been writing Cloudflare block pages to storage as evidence, according to a writeup published on dev.to [1][9]. The captures came back HTTP 200 with a file written, so nothing in the pipeline's exit status separated a real screenshot of a post from a screenshot of an error page [9].
The reason a screenshot is the product at all is worth stating. He says he built an alternative that pulled post text through an API and rendered a tidy "evidence card" image, then discarded it, on the grounds that an image you can author freely after the fact proves nothing [3]. That decision is what makes the failure mode expensive: the artifact is the evidence, so a plausible-looking artifact is worse than no artifact.
The dependency chain got there in steps. He started on Cloudflare Browser Rendering, where the wiring worked and the capture did not: X blocks headless browsers and the request times out, and YouTube refuses script injection under a Trusted Types CSP, so the comment cannot be made to render [4][5][6]. He moved to a provider with a real browser and bot avoidance behind it, and both captures started working [7].
Then the smaller silent failures. Element screenshots with selector_algorithm=clip return a blank image when the element sits below the fold: the selector matches, the capture reports success, and the file is empty [8]. Separately, every request began returning 400 one day because the provider had narrowed which timezones it accepts, after he had passed time_zone: Asia/Tokyo [10][11][12]. That one was diagnosable in minutes only because the raw error body was stored in the database under rawPayload.screenshotError [13]. His fix was to drop the parameter, leave rendering in GMT, and record the legally meaningful capture timestamp himself, in JST [14].
The block-page incident had two stacked causes. X captures were routed through a third-party embed wrapper, twitframe, whose domain had been retired with no notification [9]. And ignore_host_errors was set to true, which instructs the renderer to continue capturing even when the host returns an error [15]. He now routes X captures through the official embed at platform.twitter.com/embed/Tweet.html?id= [16], and argues that a third-party wrapper ends when its maintainer stops caring while an official embed is maintained because the platform wants it maintained [17].
The last failure came from an optimisation. Moving capture from synchronous to background made scanning faster, and saved evidence stopped having screenshots attached, because the save path assumed the screenshot already existed and copied it forward [18][19][20]. The repair is layered: scanning responds immediately and captures in the background, the save step checks for a screenshot and captures it there if missing, and anything still missed is picked up by the next scan [21].
Three of the four described failures produced no exception at all [22]. That is the operational point. If your pipeline's output is evidence, monitoring exit status buys you almost nothing; you need an assertion about the content of the file, and flags that mean "continue even if the host looks broken" are pure hazard [15]. Worth watching in your own systems: any renderer call where a non-empty file counts as success, and every stage downstream of a step you recently made asynchronous.