Modern websites detect bots using TLS fingerprints, not just HTTP headers.
Most PHP HTTP clients rely on curl, which means their TLS fingerprint always looks the same.
So I built a PHP extension that makes requests look like real browsers.
I tried scraping a website using PHP.
The first request worked.
The second request failed.
The third request got blocked.
At first I assumed the usual things were wrong. Maybe the User-Agent header. So I changed it to Chrome.
Still blocked.
Then I copied every header Chrome sends.
Still blocked.
That’s when I realized something interesting.
The server wasn’t detecting my script using HTTP headers.
It was detecting it before the HTTP request even arrived.
The Layer Most Developers Don’t See
Most developers think HTTP requests look like this:
Headers
Cookies
User-Agent
Body
But modern bot protection systems look deeper.
Before your HTTP request reaches the server, your client performs a TLS handshake.
That handshake contains details like:
- cipher suites
- TLS extensions
- ALPN negotiation
- HTTP/2 settings
Together these create a TLS fingerprint.
Different clients produce different fingerprints.
Example:
Chrome → Chrome TLS fingerprint
Firefox → Firefox TLS fingerprint
curl → curl TLS fingerprint
Even if you perfectly copy Chrome headers, the TLS fingerprint still reveals the truth.
The PHP Problem
Most PHP HTTP clients rely on libcurl.
Examples:
- curl extension
- Guzzle
- Symfony HTTP client
Which means most PHP requests share the same fingerprint:
curl
To modern bot protection systems, that’s easy to detect.
Changing headers doesn’t help because the TLS handshake still exposes the client.
The Idea
I wanted PHP requests to behave like real browsers, not curl.
But implementing a custom TLS stack in PHP is not realistic. That kind of work requires a systems language.
That’s where Rust comes in.
I discovered a Rust networking library called wreq, and on top of it a project called rnet that can impersonate real browsers at the TLS and HTTP/2 level.
It supports profiles like:
- Chrome
- Firefox
- Safari
- Edge
- OkHttp
Meaning the request genuinely behaves like those browsers.
The problem was that Rust libraries can’t be used directly from PHP.
Unless you build a PHP extension.
Bridging Rust and PHP
I used a project called ext-php-rs, which allows writing PHP extensions in Rust.
The idea became:
Rust networking stack
+
PHP extension
=
Browser-like HTTP requests from PHP
After a lot of experimentation (and fighting with build tools, BoringSSL, and PHP ABI differences), it finally worked.
Traditional PHP Request
PHP Script
↓
libcurl
↓
TLS fingerprint: curl
↓
Server detects bot
Using php-rnet
PHP Script
↓
php-rnet
↓
TLS fingerprint: Chrome
↓
Server sees a browser
Introducing php-rnet
The result is php-rnet — a PHP extension that allows PHP to send HTTP requests that look like real browsers.
Example:
$resp = RNet\get("https://httpbin.org/get");
echo $resp->text();
Or using a reusable client:
$b = new RNet\ClientBuilder();
$b->timeout(30);
$client = $b->build();
$resp = $client->get("https://httpbin.org/get");
But the interesting part is browser impersonation.
$b = new RNet\ClientBuilder();
$b->impersonate(RNet\Emulation::CHROME_136);
$client = $b->build();
$resp = $client->get("https://tls.browserleaks.com/json");
To the server, this request looks like it came from Chrome 136.
Even though it came from a PHP script.
Why This Matters
Modern bot protection systems analyze more than just HTTP headers.
They examine how clients behave at the TLS and HTTP/2 level.
Tools like php-rnet make it possible to reproduce real browser traffic from backend code.
Some possible use cases include:
- automation
- scraping
- testing bot detection systems
- research
Final Thoughts
PHP is usually seen as a high-level web language.
But combining it with Rust opens up interesting possibilities.
In this case, it allowed PHP to speak the same network language as modern browsers.
If you're curious about the project, you can find it here:
Blog post explaining the project:
https://tasli.netlify.app/blog/posts/php-requests-dont-look-like-browsers
Top comments (1)
The question I have is why create a PHP extension? Why not use a system call?
It is great to have the ease of use. But I'm more of the one tool one task philosophy. From my perspective the extension does three things:
Parts one and three aren't going to be that much more performant in a PHP extension than using string concatenation for the CLI input, and a predictable output for the CLI response in PHP.
If you can add PHP extensions to a server, it is very likely you can add other executables.
I do think it is good to show PHP applications should not use PHP solutions all the time. There are times it makes sense to use other languages.