Payload rejection and transport rejection are different failures
A payload rejection arrives as an HTTP response. The connection completed, the request reached the server and the application returned a status code. A transport rejection ends earlier: the handshake fails, the peer resets the connection, or the server closes it without answering. Your client raises a connection error and never sees a status code. Those two outcomes call for different work, so establish which one you have before touching the request body.
The phrase fingerprinting covers two layers that travel together. The transport layer is what the client says during the TLS handshake. The application protocol layer is what it does immediately afterwards, in the HTTP/2 preface and the first frames. A server can identify a client from either layer, and clients that share a library share the shape of both.
What a client fingerprint is made of
A client fingerprint is the set of implementation details a client reveals before it sends a request. The values are not secrets. They are the ordinary output of a particular HTTP stack, and they differ between stacks even when the requests are identical.
- The TLS version range the client offers and the order of the cipher suites inside it.
- The extension list, its order, and the supported groups and signature algorithms it advertises.
- The application protocol list, which shows whether the client wants HTTP/1.1 or HTTP/2.
- Whether the client sends GREASE values, which many browsers do and most language runtimes do not.
- The shape of the HTTP/2 preface: the settings frame, initial window sizes, and the order of pseudo-headers on the first request.
Every one of those values comes from the library rather than from your code, which is why a faithful copy of the request can still be refused at the door.
Telling a fingerprint block from pinning or a WAF
Three failures look similar from a script and mean different things. The location of the decision separates them.
- A fingerprint block is decided by the server. The handshake is refused or the connection is dropped, and no HTTP response follows. The same client and the same request reach a different server without trouble, so the refusal tracks the destination service rather than your payload.
- Pinning is decided by the app on the device. The application checks the certificate or public key it expects and aborts the connection itself. That failure happens on the phone, appears in the app's own logs, and never reaches a backend decision, because the app refused the connection itself.
- A WAF block is decided after the connection is up. You receive an HTTP response, usually a 403, and the body often carries a block page, a request identifier, or headers naming the vendor. The request arrived, so the payload and headers were available to inspect.
A connection reset during the handshake points at the first category. A client that connects from a desktop browser but fails from a script, against the same endpoint and the same payload, points at the first category as well, because the browser and the script differ mainly in how they negotiate the connection.
What a normal HTTP client can reproduce
Some of the fingerprint is configurable and some of it is fixed by the runtime.
You can usually set the accepted TLS versions, sometimes the cipher list, and in some libraries the ALPN list. You can sometimes adjust HTTP/2 settings such as initial window size and header table size. Extension ordering, GREASE, and the exact pseudo-header treatment are normally internal to the TLS and HTTP/2 implementation, so a standard library rarely exposes them.
That leaves three practical routes. The first is to configure what the library does expose and test whether the block clears. The second is to accept that the transport shape is fixed and treat the endpoint as reachable only from a matching stack. The third is to send the same bytes through a transport that already matches the app, which preserves the fingerprint because nothing about the handshake was rebuilt.
TLS 1.3 changed which parts are visible
Under TLS 1.3 most of the handshake, including the certificate, is encrypted, so a passive observer sees less than it did under TLS 1.2. The server still sees the full ClientHello, which is sent in plaintext, and can still classify the client from it. Where a middlebox used to read the certificate to decide, it now has to work from the connection metadata and the client's own behaviour. The app on the device is unaffected by any of this, because it is an endpoint rather than an observer, and it negotiated the encrypted session in the first place.
The useful conclusion for a reproduction project is narrow. A request body can be rebuilt from source in a language runtime. A connection identity cannot be rebuilt that way, so when the failure is at the handshake the answer is a different transport rather than a different header.
Related work
Reviewed 28 September 2026 · SReverse research desk