DEV Community

Cover image for PHP Requests Don’t Look Like Browsers — So I Built One
Taki Elias
Taki Elias

Posted on

PHP Requests Don’t Look Like Browsers — So I Built One

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Using php-rnet

PHP Script
     ↓
php-rnet
     ↓
TLS fingerprint: Chrome
     ↓
Server sees a browser
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Or using a reusable client:

$b = new RNet\ClientBuilder();
$b->timeout(30);
$client = $b->build();

$resp = $client->get("https://httpbin.org/get");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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

https://github.com/takielias/php-rnet

Top comments (1)

Collapse
 
xwero profile image
david duymelinck

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:

  • Transforms PHP to Rust input
  • Executes the request
  • Transforms Rust output to a PHP object

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.