invisible_playwright vs hrequests
hrequests pairs a TLS-impersonating HTTP client with a browser mode that injects its fingerprint into the page after launch; invisible_playwright is a single browser whose fingerprint comes from inside a patched engine itself, so there is no injection for a detector to find. hrequests, by daijro, is unusual among the tools on these comparison pages because it is two tools in one package: a TLS-impersonating HTTP client for requests that never open a browser, and an optional browser mode for the pages that need one. That hybrid design is the interesting part, and it means the comparison is really two comparisons, because each mode makes a different trade.
This page walks the two modes, names the seam each one carries, and shows where a fingerprint that lives in the browser engine lands differently. hrequests was last pushed on 2024-12-01 at time of writing, so this is a comparison of a real, working project against a different architecture, not a claim that anything is abandoned.
What hrequests actually is
hrequests wraps two capabilities behind one requests-shaped API. The first is an HTTP client that impersonates a real browser’s TLS and HTTP/2 handshake, so a plain get carries a handshake that matches the user agent it claims. The second is a browser mode, reached by rendering a response, that drives a real browser and applies a fingerprint generated by BrowserForge, daijro’s open-source fingerprint generator, at the page layer.
The usual flow is to start in the HTTP client, and only escalate to the browser when a page needs JavaScript:
import hrequests
resp = hrequests.get("https://example.com") # TLS-impersonating HTTP, no browser
if "the value you need" not in resp.text:
page = resp.render() # escalate to a real browser
html = page.html
page.close()
That escalation is the whole idea, and it is a genuinely good one for the class of target where most pages are static and a few are not. The two comparisons below are about what each half of it costs.
The HTTP mode: fast because it runs no JavaScript
The HTTP client is fast for one concrete reason: it never executes any JavaScript. It does the handshake, sends the request, and hands you the bytes. For a page whose data is already in the initial HTML, that is the correct tool and nothing heavier is justified.
The limit is the same as its speed. A page that builds its content with client-side JavaScript comes back to an HTTP client as a near-empty shell, and a page that runs a script to decide what to serve sees a client that ran no script. This is the TLS-and-headers vs a real engine split that decides whether an HTTP request is enough: matching the handshake gets you past the network layer, but it does not run the page, so anything the page computes in the browser is not there.
So the HTTP mode’s trade is explicit: maximum speed, zero JavaScript, and it works exactly up to the point where the target expects a browser to have run something.
The browser mode: fingerprints injected at the page layer
When hrequests escalates to its browser mode, the generated fingerprint is applied by injecting it into a running page, the same mechanism every init-script stealth layer uses. The fingerprint values are good, because BrowserForge generates them as a coherent statistical unit rather than a bag of independent random fields. Where they are applied is the seam.
Injection at the page layer means JavaScript runs first inside the real engine and is then overwritten. A property read after startup returns the injected value, but the act of overwriting is itself observable: a getter that now lives on the wrong object in the prototype chain, a toString that no longer reads native, a value that changed between two reads that should have been identical. This is exactly the class of tell CreepJS is built to find: it takes a clean copy of the built-ins from a fresh frame, walks descriptors and prototypes, and records an overwritten or blocked probe by name. The reported value can be perfect and the fact that it was reported by an override is still a signal.
None of this makes the injected fingerprint wrong. It makes the injection findable by a detector that looks for the seam rather than the value.
Where invisible_playwright differs: the fingerprint is in the engine
invisible_playwright does not inject a fingerprint into a page. The values are what a patched Firefox returns natively, from inside the C++ engine, so there is no later override to catch: JavaScript in the page reads the spoofed value the first time and every time, and a read is just a read. Stock Playwright drives that engine with no wrapped subset of the API to learn.
Switching from plain Playwright is two lines, and the returned object is a real Playwright Browser:
from invisible_playwright import InvisiblePlaywright
with InvisiblePlaywright(seed=42) as browser:
page = browser.new_page()
page.goto("https://example.com")
page.click("#submit") # mouse arcs to the button on a Bezier curve
The seed is the second difference. One seed derives one coherent identity - GPU, canvas hash, audio context, fonts, screen - and passing the same seed reproduces it field for field, which is what makes a failing run replayable instead of a fresh random draw every time. Behind a proxy the browser timezone is auto-derived from the egress IP by default:
proxy = {"server": "socks5://gate.example.com:1080", "username": "u", "password": "p"}
with InvisiblePlaywright(proxy=proxy, seed=42) as browser:
page = browser.new_page()
page.goto("https://example.com")
Because the TLS handshake is Firefox’s own real handshake rather than an impersonation of it, the handshake and the user agent tell the same story by construction, which is the network-layer half of what the HTTP mode gets from impersonation and the browser mode has to get separately.
The honest caveat: this is a full browser, so it is heavier and slower than a JavaScript-less HTTP fetch, and it never claims to fix the two things no browser property controls. A datacenter IP is still a datacenter IP, and machine-shaped behaviour is still a signal. A real engine removes the fingerprint seam; it does not remove your address or your mouse path.
hrequests and invisible_playwright side by side
The two hrequests modes and invisible_playwright differ on one axis that decides everything: whether JavaScript runs, and if it does, where the fingerprint lives. A mode that runs no script has no JS surface to fingerprint; a mode that injects into a running page leaves an override a detector can find; an engine that returns the value natively leaves no seam.
| What a detector checks | hrequests HTTP mode | hrequests browser mode | invisible_playwright |
|---|---|---|---|
| Runs JavaScript | No | Yes | Yes |
| Network handshake | TLS impersonation | Real browser’s handshake | Firefox’s own real handshake |
| Where the fingerprint lives | No JS surface to fingerprint | Injected into the running page | Returned natively by the engine |
| Override seam to find | None | Yes, at the injection point | None |
| Relative speed | Fastest | Full browser | Full browser |
Choosing between them
The split is clean enough to state as a rule.
Reach for an HTTP client, hrequests’ or otherwise, when the data is in the initial HTML and the target does not require a browser to have run. It is faster, lighter, and the right size for that job. Reach for a real browser when the page runs JavaScript to build or gate its content, and at that point the question is where the fingerprint lives: injected into the page after startup, or returned natively by the engine. The first is findable at the seam; the second has no seam to find.
hrequests puts both tools in one package and lets you escalate between them, which is a real convenience. invisible_playwright is only the second tool, and it moves the fingerprint out of the page and into the engine.
Conclusion
hrequests is a hybrid, and each half is a different trade. The HTTP mode is fast because it executes no JavaScript, and it stops working the moment a target expects a browser to have run something. The browser mode injects a well-generated fingerprint at the page layer, which is where a lie-detector looks for the overwrite. invisible_playwright takes the browser-mode job and changes the layer: the fingerprint is what a patched engine returns natively, driven by stock Playwright, reproducible from a seed. If your work is mostly static pages with the occasional dynamic one, the hybrid’s escalation is genuinely handy. If it is the dynamic pages that matter, the engine layer is the durable difference.
Short answers to the questions that lead here
Is hrequests abandoned? No claim here. Its last push was 2024-12-01 at time of writing, and everything above describes it as a working project with a real design.
What is the actual architectural difference? hrequests’ browser mode injects the fingerprint into a running page; invisible_playwright returns the fingerprint from inside the C++ engine, so there is no later override for a detector to catch.
Is the HTTP mode enough on its own? For pages whose data is in the initial HTML, yes, and it is faster than any browser. For pages that build content with client-side JavaScript, no, because an HTTP client runs no script.
Does injecting a good fingerprint pass a strict detector? The value can be perfect and the injection still leaves a seam - an override on the prototype chain, a toString that no longer reads native - which is exactly what CreepJS is built to surface.
Can I keep my existing Playwright code? Yes. invisible_playwright returns a real Playwright Browser, so every method works as documented upstream; the switch is two lines at launch.
Does a real engine mean I never get blocked? No. It removes the fingerprint seam. It does not change your IP reputation or your behaviour, and both of those are still measured.
Sources
- hrequests’ own repository and README, read from source, including the last-push date cited above and its description of the HTTP client plus the browser-render mode.
- BrowserForge, the open-source fingerprint generator hrequests uses in its browser mode, described from its own project.
- This project’s own quickstart and configuration docs for the API shown, and its detection notes for what an injection seam looks like to a descriptor-walking suite.
See also: when a TLS-matching HTTP request is enough and when it is not, the same generate-then-inject split in another toolkit, and what CreepJS actually inspects.
Written while maintaining invisible_playwright, a Firefox patched at the C++ level driven by stock Playwright. The comparison is with a working project; the durable difference is the layer the fingerprint lives in, not who shipped last.