github.com/sardanioss/httpcloak
v1.6.11
#1462 most downloaded on Go modules
sardanioss/httpcloak
What this package is like to depend on
Last release 7 days ago
16 Aug 2026
Release timing varies
gaps range from 8 days to 2 months
Some releases are documented
notes for 9 of 35 stable releases
4 versions withdrawn
withdrawn after publishing
7 months old
85 releases · first in 2025
85 releases in the last 12 months
see the full history below
Release timeline
85 releases · Dec 2025 to Aug 2026Releases
latest 60 of 85-
v1.6.1116 Aug 2026Release notes
Open source →HTTP/2 header compression now matches Chrome on the wire.
The header list going out was already correct, so nothing that inspects
headers could see any of this. The HPACK instructions carrying that list were
not Chrome's, in four ways, three of which fired on the very first request.- Cookie crumbs were emitted in the never-indexed representation, which no
browser uses. Nothing in that form is ever stored, so a large cookie jar was
re-sent in full on every request rather than being referenced in a single
byte per crumb after the first. On a session carrying a large jar that was an
~880 byte header block on every request where a browser settles near 35, and
it persisted for the life of the connection. - The authority pseudo-header had the same problem for the same reason.
- The path and method pseudo-headers referenced the wrong static table entry
whenever their value was not one of the two the table happens to carry, which
covers any path other than / and any method other than GET or POST.
All four verified byte for byte against captures of the browser itself rather
than against a specification. A connection that gets reused now settles into
the same small, fully-referenced header block a browser produces and holds
there, measured flat over sixty consecutive requests.Also in this release: a per-hop redirect callback, 307 and 308 no longer lose
the request body through the root session, and a data race on the pooled
connection use counter.Two things to know. Anyone recording the exact bytes of an outgoing header
block will see them change. And compression state is now shared across requests
on a connection the way a browser shares it, which is what makes the small
blocks possible; the never-indexed list is still configurable per profile, it
simply no longer defaults to being populated.TLS is untouched. JA4 and the HTTP/2 settings fingerprint are unchanged.
Full notes in CHANGELOG.md.
Release notes
Open source →Added
-
Request.OnRedirectdecides each redirect hop before it is taken: redirect policy was two scalars, follow-or-not and a cap, so a caller who needed to stop on one particular hop had only one option — turn following off and re-implement the chain, including the method rewrite, theRefererpolicy, the cookie jar and the credential scrubbing that make following a redirect correct in the first place. TheRequesttypes in the root,clientandtransportpackages now carry anOnRedirect func(*Redirect) errorthat is called once per hop before the follow-up request is built. Returnnilto follow it,ErrUseLastResponseto stop the chain and get the 3xx back as the response with a nil error, or any other error to fail the request — that error comes back unwrapped, so anerrors.Isagainst your own sentinel matches what you returned. TheRedirectit receives carries the hop number, the 3xx's status and its headers, the URL that produced it, the resolved target, the method the next hop will use, and whether the hop crosses an origin or downgrades the scheme. The headers matter more than they look: aSet-Cookieor a routing header emitted on hop two is invisible to anyone who only sees the final response, and the two origin flags are there so a decision is made on a parsed comparison rather than onstrings.Contains(To, "example.com"), which also passes forhttps://example.com.attacker.test. The hop is deliberately read-only — a callback able to rewrite the target would sit upstream of the origin scrubbing, which is exactly what keepsAuthorizationfrom following a redirect off-origin. It is not called for a 3xx with noLocation, because there is no hop to veto, nor for the hop that would exceed the cap, because letting a veto answer that would quietly turn a "too many redirects" error into a success. Leaving the field nil changes nothing. -
Redirect.GetHeader/Redirect.GetHeaders: the 3xx headers a redirect callback receives are lowercase-keyed, like every other header map in the library, so indexing the map with the spelling people actually write (Headers["Set-Cookie"]) returned nothing at all. These two read it case-insensitively, matchingResponse.GetHeader/Response.GetHeaders; use the plural forSet-Cookie, which legitimately repeats. -
Request.GetBodyre-opens a body that has to go out twice: needed only when the body is a genuine stream. For*bytes.Reader,*bytes.Bufferand*strings.Readerone is derived automatically at no cost, since the bytes are already in memory. It returnsio.Readerrather thannet/http'sio.ReadCloseron purpose: the value is handed tohttp.NewRequestWithContext, and it is that function's type switch on the concrete reader type that setsContent-Length. Anio.NopCloserwrapper hides the type, the length comes out unknown, and the request goes out chunked — so matching the standard library's signature exactly would have made a replayed hop use a different framing from the hop before it, which is a wire difference no browser produces.
Fixed
-
HTTP/2 header compression did not match Chrome, and the gap widened with every request on a connection: the header list going out was correct, so nothing that inspects headers could see any of this. The HPACK instructions carrying that list were not Chrome's, in four separate ways, and three of them fired on the very first request.
The largest concerned cookies. They were crumbled correctly, one field per cookie-pair as a browser does, and then emitted in the never-indexed representation, which tells every intermediary the value is sensitive and must never be added to a compression table. No browser emits that. Chrome indexes cookie crumbs like any other header, which is why a browser sends a large cookie jar in full exactly once per connection and then refers back to it with a single byte per crumb for the rest of the connection's life. Because a never-indexed field is never stored, the entire jar was instead re-transmitted in full on every single request. Measured against a session carrying a bot-management-shaped jar, the request header block stayed near 880 bytes on every request where a browser settles at about 35, so roughly a 25x difference that grew with the size of the jar and persisted for as long as the connection did. Over a couple of thousand requests that is well over a megabyte of header traffic no browser would ever produce.
The authority pseudo-header had the same problem for the same reason: never stored, so re-sent as a full literal on every request instead of being referenced in one byte from the second request onward. The path and method pseudo-headers referenced the wrong entry of the static table when their value was not one of the two the table happens to contain, which put a different byte on the wire for any path other than
/and for any method other than GET or POST.All four now match a real browser byte for byte, verified against captures of the browser itself rather than against a specification. A session that reuses a connection now settles into the same small, fully-referenced header block a browser produces, and stays there: measured over sixty consecutive requests it holds flat with no growth or churn.
Two consequences worth knowing. Anyone who was recording the exact bytes of an outgoing header block will see them change. And header compression state is now shared across requests on a connection the way a browser shares it, which is what makes the small blocks possible; if a profile genuinely needs the old behaviour for a non-browser client, the never-indexed list is still configurable per profile, it simply no longer defaults to being populated.
-
A 307 or 308 through the root session silently sent no body at all:
transport.Requestcarries a body in two fields,Body []byteandBodyReader io.Reader, andSession.Dopopulates only the second, because the publicRequest.Bodyis anio.Reader. The redirect path copied only the first. So the code said "307/308 preserve body" and did the opposite: the next hop went out empty, with the caller'sContent-Typestill on it, because the method is unchanged on those two codes and nothing strips it. There was no error, no short write and no truncation warning — the hop simply arrived with nothing in it and usually came back 200, which is why this survived so long. A POST that a payment or checkout endpoint answers with a 307 was the common way to meet it. The redirect path now re-opens the body fromGetBody, carriesGetBodyonto the hop so a second 307 in the same chain also works, and refuses the hop withErrBodyNotReplayablewhen the body is a one-shot stream with no way to re-open it, handing back the 3xx alongside the error so the caller can read itsLocationand drive the rest themselves. Sending a request the caller believes carries a body, without the body, is not a thing to do quietly.client.Clientnever had this bug — it buffers the body up front. -
A retried request re-sent an exhausted body: the same root cause on the other path. The retry loop re-sends the same request object, so an attempt after the first streamed from a reader the previous attempt had already drained, and the server got the declared
Content-Lengthwith nothing behind it. The body is now re-opened for each attempt. Where a redirect refuses to proceed, a retry simply does not happen: the caller already has a response or a real error in hand, so a body that cannot be replayed disables the retry rather than failing the request. -
A stale pooled HTTP/1.1 connection turned any request with a body into an empty one: the transport takes an idle connection from the pool, writes the request, and on failure closes it and retries on a fresh one. The retry reused the same
*http.Request, whose body the failed attempt had already streamed out, so it went out with theContent-Lengthit had computed and no bytes behind it — the server then waited for a body that never arrived, and neither end reported anything wrong. Reachable by any POST that happened to draw a connection the origin had closed, and the reason the 307 fix above did not work on its own: the very next hop usually draws exactly such a connection. The body is now re-opened before the retry, and a body that cannot be re-opened surfaces the connection error rather than sending a corrupt request. -
A redirect hop dropped almost every per-request option: the follow-up request was built from four fields — method, URL, headers and header order — so
TLSOnly,DisableConditionalCache,DisableClientHints,DisableHighEntropyClientHintsand the per-requestTimeoutwere silently discarded after the first hop, and a per-requestFollowRedirects: &trueagainst a session that defaults to not following stopped the chain after one hop. A request that opted out of client hints got the opt-out on hop zero and the hints back on hop one, which is a fingerprint that changes mid-chain. All of them now ride along. TheTimeoutis a clamp rather than an extension: one overall deadline for the whole chain is still established at the first hop, andcontext.WithTimeoutkeeps the earlier of the two. -
Exceeding the redirect cap threw the last response away: it returned a bare
errors.New("too many redirects"), so a caller could neither match it witherrors.Isnor see where the chain had got to. It is nowErrTooManyRedirects, and the session hands back the response carrying the lastLocationalongside it, the waynet/httpdoes whenCheckRedirectfails. Nothing is leaked by ignoring it: the body is already buffered, so closing it is a no-op. Theclientpackage adopts the same sentinel but not the response, because at the point it detects the cap it has not built one yet. -
Intermediate 3xx responses were never closed on the way through a chain. Harmless today, since the transport buffers each body before returning and hands back a no-op closer, and it stops being harmless the moment response streaming reaches that path.
-
A data race between concurrent requests sharing a pooled connection: the pool records a use count on every connection it hands out, writing it under the connection's mutex, but several callers read that counter back with no lock. On HTTP/2 and HTTP/3 one connection carries many requests at once, so two of them would collide, one inside the guarded write and the other reading bare. All reads now go through an accessor that takes the same mutex. Only the reported connect-time breakdown was ever at stake, never whether a request is sent or what it contains, but it is a real race and it failed builds run with the race detector.
- Cookie crumbs were emitted in the never-indexed representation, which no
-
v1.6.11-0.20260814171422-133bb5c729b914 Aug 2026 pre-releaseNothing published for this version
-
v1.6.11-0.20260814170403-91f3d4c4335c14 Aug 2026 pre-releaseNothing published for this version
-
v1.6.1014 Aug 2026Release notes
Open source →First published release since 1.6.8 on PyPI, npm and NuGet. On the Go module
proxy it supersedes v1.6.9, which is retracted: that tag was pushed from a
pre-fix commit and proxy.golang.org has it permanently pinned there, serving a
build where TLS verification fails open on HTTP/3.Highlights
-
Chrome 151 across Windows, Linux, macOS and Android, with chrome-latest*
repointed. Verified byte for byte against real captures over both TCP and
QUIC. The iOS profile ships as provisional and chrome-latest-ios stays on the
confirmed 150 build. -
Certificate verification callbacks, so certificate pinning is possible
(#85). Supplied TLS configuration used to be accepted and then ignored;
callbacks now run on all three protocols, including HTTP/3, where they
previously failed open. -
Saving a session no longer quietly weakens its certificate checks. A session
saved with verification configured now refuses a plain restore rather than
coming back with the permissive half only. -
Long downloads are no longer cut off after roughly two minutes (#83).
-
Response bodies could be silently corrupted under concurrency. Fixed.
-
The HTTP/3 handshake now matches a real Chrome capture parameter for
parameter, and no longer opens a throwaway TCP connection before the request. -
Headers the preset reserves no slot for keep a stable order instead of a
randomised one, on all three protocols. -
The documented local-proxy pattern applied no fingerprint at all, in every
language. Requesting an https:// URL through the proxy tunnels past it. All
guides, readmes and examples are corrected, and .NET gains
LocalProxy.CreateClient().
Full notes in CHANGELOG.md.
Release notes
Open source →Added
-
Response TLS details: a response can now carry the negotiated TLS information for the connection that produced it (protocol version, cipher, negotiated application protocol, and the leaf certificate's subject, issuer, names, validity and fingerprint). It is off by default and requested per request, because parsing certificate fields is not free and most callers do not want to pay for it on every response.
-
Preset constants for the newest profiles in every binding: the typed preset tables in the Python, Node and .NET packages had fallen two releases behind, so neither Chrome 150 nor Chrome 151 nor the per-OS Firefox profiles were reachable from a named constant in any of them. All are now present.
-
Chrome 151: asking for the latest Chrome now gives you Chrome 151 on Windows, Linux, macOS and Android, and the matching
chrome-latest*profiles point at it. Everything below the header layer is unchanged from Chrome 150, including the post-quantum signature algorithms, and that was confirmed against real Chrome 151 captures over both TCP and QUIC. Worth noting for anyone building profiles by hand: the browser's brand list is not a simple version bump between 150 and 151. The separator characters, the filler brand's version and the order the three brands appear in all changed at once, so the value has to be transcribed from a capture rather than derived. A Chrome 151 iOS profile ships too, but iOS spells its version out in full and that build number cannot be derived, so it is provisional andchrome-latest-iosdeliberately still resolves to the confirmed 150 profile until a capture lands. -
LocalProxy.CreateClient()andLocalProxy.CreateFingerprintHandler()on .NET: building a client from the proxy now actually fingerprintshttps://requests, which pointing a plain proxy handler at the proxy URL never did (see below).CreateClient()takes an optional handler of your own, so cookies, credentials and decompression settings survive, andCreateFingerprintHandler()covers the case where something else owns the client, such as a factory or a library that takes a handler. -
Certificate verification callbacks (#85): you can now supply your own certificate checks, either individually or by handing over a standard TLS config, and they work the same way the standard library's do: one callback receives the raw certificates and any chains that were built, the other receives the completed connection state, and returning an error from either aborts the handshake. This is what you want for certificate pinning. Only the verification parts of a supplied TLS config are read; the parts that would reshape the handshake are deliberately ignored, because honouring them would silently change how the client looks on the wire, which is the one thing this library exists to keep stable.
-
Per-OS Firefox profiles:
firefox-133andfirefox-148now ship-windows,-linuxand-macosvariants, with matchingfirefox-latest-*aliases, so you can pin Firefox-on-Windows from a Linux host without hand-overriding the User-Agent. The plainfirefox-133/firefox-148names still follow the host OS, so nothing changes for existing code. Unlike Chrome, the variants differ in the User-Agent only: Firefox does not vary the rest of its fingerprint by operating system, so the TLS bytes, HTTP/2 settings and header order are identical across all three, and that is locked by a test. -
Request.HeaderOrdersets the header order for a single request, without touching the session:SetHeaderOrderis session-wide state behind a mutex, so pinning an order for one request meant set, send, restore, and every concurrent request on that session had to be serialized around the window where the override was live, or it went out under someone else's order. That made the documented ordering control unusable on a session doing parallel work, which is exactly where it was needed: adding a single header a browser never sends (an API token, a signature) leaves it in the sorted tail unless you can say where it belongs. TheRequesttypes in the root,client, andtransportpackages now carry aHeaderOrderfield that applies to that request alone and overrides whatever is installed session-wide. It is read from the request rather than from shared state, so no lock is taken and concurrent requests can each carry a different order. The semantics matchSetHeaderOrderexactly, the list is a prefix, the preset's position table still covers every header you leave out, and a stable sorted tail catches the rest, so naming one header costs you nothing for the others, and naming all of them gives you the exact wire order. Names are case-insensitive. The order carries across followed redirects, matching the way the redirect path already replays your headers onto each hop: without that, a header you slotted explicitly would still be sent on the next hop but re-placed by the preset table, leaving the header set and its order disagreeing mid-chain. Leaving the field unset changes nothing.
Changed
-
The Go
client.ClientAPI now sends the same TLS handshake as the session API: the client hello is assembled in two places internally, and only one of them applied a profile's signature-algorithm override. A caller using theclientpackage with a Chrome 150 or 151 profile therefore sent that browser's headers and User-Agent alongside a handshake missing its post-quantum signature entries, which is an internally inconsistent fingerprint and exactly the kind of contradiction this library exists to remove. Both paths now agree. This changes the handshake those callers send: it moves to the correct value, confirmed against a real capture, but anyone recording or pinning the previous hash will see it change. Profiles that carry no signature-algorithm override are unaffected, byte for byte. -
A partial
SetHeaderOrdernow extends the preset's order rather than replacing it: the list you passed used to replace the preset's ordering table wholesale, which meant every preset header you did not name joined the randomised remainder described above, so reaching for the documented ordering control made your fingerprint worse than leaving it alone. Your list is now a prefix override: the headers you name come first in the order you gave them, then the preset's own table covers everything you did not name, then anything still unplaced follows sorted by name. Passing a complete order list behaves exactly as before, and passingnilor an empty list still resets to the preset default. -
Response.Location()resolves the redirect target the waynet/httpdoes: the standard library'shttp.Responsehas aLocation()method that parses theLocationheader into a*url.URLand resolves it against the request URL, so a relative/logincomes back as the fullhttps://host/login. httpcloak responses only exposed the raw header string, which left anyone inspecting a 3xx with redirects disabled to re-implement that resolution by hand. TheResponsetypes in the root,client, andtransportpackages now carry the same method: it parses the header, resolves a relative target against the URL that produced the response (FinalURL), and returns the package'sErrNoLocationsentinel when noLocationheader is present, matchingnet/httpsemantics exactly, which is locked by a parity test that runs the same cases through both implementations.
Fixed
-
The local proxy silently truncated every compressed response: it decompresses a body before handing it back, and correctly dropped the header saying the body was compressed, but kept the origin's byte count, which counts the compressed bytes. The caller therefore stopped reading that many bytes into a larger body and treated the response as finished. A 68 KB page compressed to a few hundred bytes arrived as a few hundred bytes of plaintext, with no short read, no decode failure and no error anywhere. Anyone pointing the local proxy at a compressed origin that declares a length has been getting quietly wrong data; it went unnoticed because the endpoints usually used to exercise this path serve either uncompressed or chunked responses. Both headers are now dropped together and the response is delimited by connection close.
-
The documented local-proxy pattern applied no fingerprint at all, in every language: the local proxy fingerprints a request only when it arrives as a proxy-style plain HTTP request. Ask any mainstream client for an
https://URL through a proxy and it opens a tunnel instead, then performs its own TLS handshake straight through to the destination, so the destination sees the calling client's handshake and the proxy is left relaying bytes it cannot read. Nothing errors and a real response comes back, which is why this went unnoticed: the quick-start snippets in the guides, both binding readmes, the .NET example and its test all taught exactly that shape, under comments stating the fingerprint was being applied. Measured against a fingerprint reflector, the tunnelled requests were byte-identical to the same client with no proxy configured at all. The escape hatch already existed and was already described in the reference section: request the target as plain HTTP and add the scheme-upgrade header, which keeps the handshake on the proxy side. Every snippet across the guides, both readmes and both examples now uses it, with the measurements alongside so the difference is checkable rather than asserted. The same applies to the per-request session header, which on a tunnelled request travels inside the tunnel where the proxy cannot read it, so session routing was silently doing nothing too. .NET gets a code-level fix as well, see above; for the other languages this is a documentation correction, and anyone who built on the old snippets should assume those requests were never fingerprinted. -
Non-browser profiles no longer advertise a browser-specific HTTP/3 parameter: the switch deciding whether to send Chrome's browser-specific transport parameters was reading a field that actually selects parameter ordering, and that field defaults to the Chrome setting for any profile that does not override it. A Firefox profile forced onto HTTP/3 therefore sent a Firefox-shaped handshake carrying a Google-only parameter, which contradicts itself to anyone reading it. The decision now follows the profile's own client identity.
-
Supplied TLS configuration was accepted and then ignored (#85): the option to pass a TLS configuration already existed, so code that set verification callbacks compiled and looked correct, but nothing ever read it and the callbacks never ran. Anyone relying on them for certificate pinning was performing no extra verification at all while believing they were. They now run, and rejecting a certificate genuinely fails the request.
-
Long downloads no longer get cut off after about two minutes (#83): for HTTP/2 a request is considered finished as soon as the response headers arrive, but the body can keep streaming for minutes afterwards. The connection pool was only tracking the first part, so a large or slow download looked completely idle and the pool would close the connection out from under the reader, roughly two minutes in. Connections are now held for as long as the body is actually being read, and a connection is never closed while a response is still streaming on it, however old it looks. A body that is abandoned without being closed is still reclaimed after a long idle period, so this cannot leak connections.
-
Response bodies could be silently corrupted under concurrency: response bodies were read into buffers taken from a shared pool, and the buffer was handed to the caller while still belonging to that pool. On one path it was returned to the pool while the response still pointed into it, so a later request could overwrite a body that had already been handed out. There was no crash and no error, just wrong bytes. Bodies now always belong solely to the caller.
-
Two QUIC transport settings were being dropped, and two more were being sent that no browser sends: on HTTP/3 connections the configured datagram frame size and the browser-specific transport parameters were lost before the handshake was assembled, so the connection advertised an internal default and omitted two parameters a real browser always includes. Separately, the code built two parameters that a real Chrome does not send at all; they never actually reached the wire, because the same dropped-configuration bug discarded them too, so removing them is a correctness cleanup rather than a change anyone could observe. One of them was worse than a stray value: producing it required opening and timing a throwaway TCP connection to the destination before the HTTP/3 request, which is something a browser never does. That connection is gone. All four are corrected, and the HTTP/3 handshake now matches a real Chrome 151 capture parameter for parameter. This changes the HTTP/3 handshake for every Chrome profile, not just the newest one.
-
Headers the preset reserves no slot for now keep a stable order instead of a random one: any header outside the impersonated browser's ordering table, custom
X-headers, API tokens, signature headers, fell through to Go's map iteration in both the HTTP/1.1 writer and the HTTP/2 header encoder, and that iteration is deliberately randomised. The same four custom headers produced eight distinct arrangements across eight requests on HTTP/1.1, and seven across eight on HTTP/2. A header order that changes on every request is itself a strong signal, because no browser produces one, and it landed on exactly the requests most likely to be carrying something worth checking. Every header is now named up front, so nothing reaches the randomised fallback, and HTTP/1.1, HTTP/2 and HTTP/3 all agree on the result. -
Certificate verification could be skipped entirely on HTTP/3: the verification callbacks added in this release were never invoked on an HTTP/3 connection. That failed open rather than closed - a callback that is never consulted can never reject - so a caller pinning a certificate believed they were pinned while the HTTP/3 path accepted anything the system trust store accepted, and on a client that prefers HTTP/3 that is the connection the request actually used. Two separate causes, both fixed: the conversion into the QUIC handshake rebuilt the TLS configuration field by field and dropped both callbacks, and HTTP/3 builds its configuration once up front rather than per connection, so installing the callbacks afterwards never reached it. Both now work, and a rejection genuinely aborts the connection. A restricted set of trust anchors was being dropped the same way, with the same fail-open consequence, and is now honoured.
-
Saving and restoring a session no longer quietly weakens its certificate checks: verification callbacks and a restricted set of trust anchors are a function and an in-memory structure, so a saved session could never carry them. The flag that turns the built-in chain check off is a plain boolean and saved perfectly well. So a session configured as "skip the built-in check, I verify the certificate myself" came back as "skip the built-in check", with nothing in its place: weaker than pinning, and weaker than plain verification, with no error anywhere to say so. A saved session now records which of these were configured, and restoring one without them fails instead, naming exactly what is missing. This is a behaviour change for anyone restoring such a session: the plain load now returns an error where it used to return a working session. New loader variants take the callbacks and the trust anchors back so the session is restored with the posture it was saved with; supplying only some of them is refused for the same reason. Sessions saved without any of this load exactly as before, files written by older versions keep working, and files written now still load on the previous release. The bindings cannot install these callbacks in the first place, so nothing there is affected.
-
Cancelling a request on HTTP/1.1 now takes effect immediately: the HTTP/1.1 path never looked at the request context once the exchange started, so a caller who cancelled waited out the full response timeout and then got back an opaque network error that did not identify itself as a cancellation. Cancelling now interrupts the exchange, including during the response body, and reports the caller's own cancellation. Connections interrupted this way are dropped rather than reused, since their position in the response stream is unknown.
-
Proxy handshakes are bounded by the request context: negotiating a tunnel through an HTTP or SOCKS5 proxy performed unbounded reads and writes, so a proxy that accepted the connection and then went silent held the request until the operating system gave up, with no way for the caller to interrupt it.
-
Browser-specific request metadata is no longer added to profiles that do not send it: an API-shaped request had a set of browser navigation headers rewritten onto it to keep a browser coherent. Applied to a profile describing a non-browser client, that did not fix an inconsistency, it invented headers that client never sends. Profiles whose header set declares none of them are now left alone.
-
The .NET cancellation lifecycle no longer leaks: cancellation registrations were never disposed, so a long-lived cancellation source driving many requests accumulated one per request, and cancelling did not release the corresponding native callback slot. An already-cancelled token now short-circuits without issuing the request.
-
-
v1.6.914 Aug 2026 withdrawnVersion retracted: Tagged from a pre-fix commit: TLS verification fails open. Use v1.6.10.
Release notes
Open source →Chrome 151 across Windows, Linux, macOS and Android, with chrome-latest*
following it. Per-OS Firefox profiles. Certificate verification callbacks for
pinning. Response.Location(). Per-request header order.Fixes both halves of the long-download problem: connections are held for the
response body's lifetime in both connection pools, so a large or slow transfer
is no longer cut off around two minutes in. Response bodies could be silently
corrupted under concurrency; HTTP/1.1 ignored context cancellation; proxy
handshakes were unbounded; the local proxy silently truncated compressed
responses.Two changes are visible on the wire. The Go client package now sends the same
handshake as the session API, which moves its fingerprint to the correct value
for profiles carrying a signature-algorithm override. A partial header order is
now a prefix that extends the profile's own table rather than replacing it.See CHANGELOG.md for the full list.
-
v1.6.812 Jul 2026Release notes
Open source →Added
- Chrome 150 across desktop, iOS and Android, with post-quantum signatures where the real browser sends them: asking for the latest Chrome now gives you Chrome 150 on Windows, Linux, macOS, iOS and Android, and every
chrome-latest*profile points at it. On the platforms that run Chromium's own stack (the three desktops and Android) the TLS handshake now advertises the post-quantum signature algorithms the current Chrome offers, so the signature list matches a real browser on the wire; iOS runs on the system stack and, exactly like the real thing, does not advertise them. Because the library keeps everything on the wire configurable, custom profiles get a new per-protocol control to add or drop the post-quantum signatures independently for the TCP and the QUIC handshakes, so you can opt in or out to match whatever you need. The desktop and iOS wire fingerprints were confirmed against real captures across the Python, Node and C# bindings; the Android one is derived from the shared desktop stack.
Fixed
-
Forced HTTP/3 no longer hangs when a QUIC path goes quiet after connecting: in HTTP/3-only mode there is no other protocol to fall back to, so a connection that finished its handshake but then stopped delivering the response (for example a network that quietly drops the larger response packets over IPv6 while keep-alives still flow) would leave the request waiting for the whole timeout. The request now bounds the wait for the first response, and if the path has stalled it drops that connection and retries once on a fresh one, preferring the other address family. Healthy connections and streaming bodies are untouched, so there is no cost on the normal path.
-
A pointed encrypted-hello config domain can no longer stall the whole request (#74): when a session was aimed at an ECH configuration domain that did not actually front the real target, the TLS handshake could sit blocked for the entire request budget. The encrypted-hello attempt is now given its own short deadline, and if it stalls the session retries once in the clear and remembers the host is incompatible so later requests skip it.
-
Streaming works against servers that only speak HTTP/1.1 (#75, #77): the streaming path had no HTTP/1.1 fallback, so a server that negotiated plain HTTP/1.1 broke streaming requests and then retried straight back into the same mismatch. Streaming now falls back to HTTP/1.1 cleanly.
-
The C# handler and the session produce the same fingerprint (#79):
HttpCloakHandlerrouted its requests differently fromSession, so the two could look like different clients on the wire. The handler now goes through the same path as the session. -
The IPv6 TCP fingerprint is complete (#81): the outgoing SYN packet kept the operating system's default IPv6 hop limit instead of the value the impersonated browser's OS uses. It now carries the right one.
-
The advertised TCP window size matches the fingerprint (#73): the window value in the SYN now lines up with the rest of the fingerprint. The window scale factor stays fixed by the host operating system's socket interface, which is a platform limitation rather than something the library can set.
-
A broad networking robustness pass: DNS now honours the real record TTLs, caches negative answers, and collapses duplicate concurrent lookups for the same host; proxy dialling tries every resolved address rather than the first, and MASQUE tunnels are keyed per host; every request path is now bounded by the configured timeout from start to finish; and the native library layer was hardened against crashes and memory issues under concurrent use.
-
Python 3.14 free-threaded (no-GIL) builds are supported (#80).
- Chrome 150 across desktop, iOS and Android, with post-quantum signatures where the real browser sends them: asking for the latest Chrome now gives you Chrome 150 on Windows, Linux, macOS, iOS and Android, and every
-
v1.6.8-beta.1.0.20260708073506-223b1acf094c08 Jul 2026 pre-releaseNothing published for this version
-
v1.6.8-beta.1.0.20260605183152-165b7124f3d505 Jun 2026 pre-releaseNothing published for this version
-
v1.6.8-beta.105 Jun 2026 pre-releaseRelease notes
Open source →Fixed
- High-entropy client hints are now coherent with the rest of the fingerprint, and you can turn them off: once a host asks for the detailed UA client hints (it advertises
Accept-CHfor things likesec-ch-ua-full-version-list,sec-ch-ua-platform-version,sec-ch-ua-arch), the session started sending them on the next request. The problem was that those detailed hints were built from a stale hardcoded table that had fallen behind: the full version list reported an older browser version with a mismatched brand token and brand order, while theUser-Agentand the always-onsec-ch-uareported the current version. A server that reads both could see the contradiction and tell the client apart from a real browser. On Linux the platform version was also a made-up value where a real browser sends an empty one. All of these now come straight from the chosen preset, so the detailed hints always line up withsec-ch-uaand theUser-Agent: same version, same brand names, same order, same GREASE brand token, with Linux sending the empty platform version like the real browser does. Adding a future browser version is now a one-place change. Two more fixes ride along: the detailed hints are now sent on streaming requests too (the streaming path used to skip them, so a host that saw both a normal and a streaming request from the same session got the hints on one and not the other), and overriding anysec-ch-*header per request now reliably wins instead of sometimes losing to the injected value. Finally there are real controls to stop the library adding these headers at all: a session option (and per-request flag) to drop everysec-ch-*header except the ones you set yourself, and a softer one that keeps the always-onsec-ch-ua/sec-ch-ua-mobile/sec-ch-ua-platformtrio but suppresses only the detailed hints, both with runtime toggles, wired through Python, Node, and C#. Locked by a coherence test across every browser preset plus end-to-end checks for the opt-outs, the streaming parity, and the override behaviour.
- High-entropy client hints are now coherent with the rest of the fingerprint, and you can turn them off: once a host asks for the detailed UA client hints (it advertises
-
v1.6.703 Jun 2026Release notes
Open source →Fixed
- Redirects now send a browser-like Referer on each hop (#70): when a session followed a redirect it carried whatever Referer was already on the request (usually none) instead of synthesizing one from the previous URL the way a browser does, so a server inspecting the Referer across a redirect chain could tell httpcloak apart from a real browser. The session now follows Chrome's default
strict-origin-when-cross-originpolicy on every hop: a same-origin redirect sends the full previous URL (with the fragment and any credentials stripped and a default port dropped, exactly as Chrome serializes it), a cross-origin redirect on the same secure scheme sends the previous origin only (for examplehttps://example.com/), and an https to http downgrade sends no Referer at all. Locked by a table-driven policy test plus a local end-to-end check against a redirecting server. - Auto mode over a UDP-capable proxy no longer stalls several seconds per request when the proxy can't relay QUIC (#68): with a SOCKS5 proxy and the default auto protocol, the session tried HTTP/3 first and waited for the QUIC handshake to finish before it would even consider HTTP/2. When the proxy accepts the UDP associate but does not actually relay QUIC datagrams (common with many residential and mobile proxies), that handshake idled out at the QUIC timeout (around 5 seconds) on every single request before falling back to HTTP/2, so a proxy that worked fine on HTTP/2 felt several times slower than it should. The proxy auto path now races the HTTP/3 and HTTP/2 connection attempts in parallel and uses whichever connects first, then caches that choice per host so later requests skip the race. When QUIC cannot get through, HTTP/2 wins in well under a second instead of after the stall; when QUIC does work, HTTP/3 still wins and you keep the better fingerprint. The same racing now covers streaming requests through a proxy, which had the identical first-try-HTTP/3 stall. As part of this the HTTP/3 reachability probe was made proxy-aware: it used to dial the target directly, which over a proxy both skipped the relay (so it could expose the real client address) and never actually tested whether QUIC relays through the proxy. It now tunnels through the proxy the same way real requests do. Verified with a SOCKS5 harness that the probe traffic reaches the proxy relay and never the target, and that a proxy which blackholes QUIC falls back to HTTP/2 in milliseconds rather than seconds.
- Request and session timeouts are now honored on connection setup (no more 30s hangs through a stalling proxy): a request through a proxy that accepts the tunnel but whose upstream never responds (a stalled or dead residential IP, a slow CONNECT, a blackholed peer) could ignore your timeout and hang up to the transport's hardcoded 30s default before failing. Three independent gaps caused it, all fixed: (1) the Go-native
Session.Do/DoWithBodysilently dropped the per-requestRequest.Timeout, so it never reached the transport; (2) the session-level timeout (Session(timeout=...)/WithSessionTimeout) was stored but never wired to the transport, so it stayed on the 30s default no matter what you set; (3) protocol fallback (auto mode trying HTTP/2 then HTTP/1.1) re-derived a fresh timeout budget per attempt, so the budgets added up and a 4s timeout could ride to 8 to 12 seconds. Now the per-request timeout reaches the transport, the session timeout acts as the default deadline, and the whole request including any fallback is bounded by one overall deadline. Verified with a stalling-proxy harness: a request that used to ride to 30s now aborts at exactly its configured timeout, and the auto HTTP/2 to HTTP/1.1 fallback no longer doubles it. If you were passing a per-request timeout as a workaround it now does what you expect, and the session-level timeout works on its own too. - Long-running HTTP/3 sessions no longer get stuck spamming handshake errors until restart (stale ECH config): a session kept alive for many hours could suddenly start failing every HTTP/3 request to a host with
illegal parameterrejections or handshake timeouts, all at once, and only a process restart cleared it. Cause: the HTTP/3 transport cached the host's ECH (Encrypted Client Hello) config and pinned it for the lifetime of the session with no expiry and no way to drop it. When a CDN rotates its ECH keys (which happens on a schedule, so independent servers all break at the same minute), the pinned config is stale, the server rejects every handshake built from it, and nothing refetched a fresh one. Now the cached ECH config carries a short TTL so a long-lived session refetches periodically, and it is dropped immediately when a handshake is rejected in a way that looks like a stale config, so the session self-heals on the next request instead of needing a restart. The DNS-level ECH cache also stops serving an indefinitely-expired config when a refresh lookup fails (it now falls back to no-ECH past a short grace window, which still connects). If you want to sidestep ECH entirely,disable_ech/disableEchon the session still does that. - HTTP/3 concurrent requests on one session no longer race or corrupt each other: firing several requests in parallel on a single session with HTTP/3 enabled (the default) could drop POST bodies, cancel requests, or hang them. The HTTP/3 transport reused one TLS ClientHello template across every QUIC connection, and the TLS layer rewrites that template in place while building each handshake, so concurrent connections stomped on each other (a genuine data race, confirmed with the Go race detector). HTTP/2 already generated a fresh template per connection; HTTP/3 now does the same. Each connection builds its own template from the same stable seed, so the fingerprint (JA3/JA4) stays byte-identical between connections, while connections no longer share mutable state. Forking a session per worker was the previous workaround and still works, but it is no longer required just to run concurrent requests. Verified race-clean under the race detector across every HTTP/3 dial path, with the TLS fingerprint confirmed unchanged.
- Cookie header now splits into one field per cookie-pair on HTTP/2 and HTTP/3 for Chrome (and Firefox): this was a mistake on my end and it is now fixed properly. Real Chrome and Firefox do not send the cookies as a single
cookie: a=1; b=2; c=3line on H2/H3. They split it into onecookiefield per pair (cookie: a=1,cookie: b=2,cookie: c=3) for header-compression efficiency, which RFC 9113 section 8.2.3 allows. httpcloak's Chrome presets were sending a single coalesced field, on the assumption that Chrome coalesces. That assumption was wrong: I had reasoned it from the wrong layer and locked it in without checking against a real capture, so any server that counts the decoded cookie fields could tell the difference. Chrome and Firefox presets now crumble the cookie into per-pair fields on both H2 and H3, matching the browser exactly (split on each;, drop one following space, kept contiguous in the cookie slot and marked never-indexed). Safari stays a single field, which is correct for WebKit. The split follows the jar and any caller-suppliedCookieheader alike. Thanks to the folks who reported this and pointed at the RFC. Verified on the wire for Chrome, Firefox, and Safari across both protocols. - Async POST / PUT / PATCH / DELETE no longer corrupt binary bodies (Python + Node): two parallel bugs that silently mangled any non-text upload through the async path. Python's
request_asyncran multipart throughbody_bytes.decode("latin-1")(lossy on the JSON-encoding side) and rawbytesthroughdata.decode("utf-8")(raised on any non-UTF-8 byte); Node'spost()/request()async didbody.toString("utf8")on anyBufferbody. Both bindings now base64-encode binary payloads and setbody_encoding="base64"on the request config so the cgo boundary preserves every byte. Sync paths and text bodies are unchanged. cgo'spost_asyncentry point gained a matchingbody_encodingfield onRequestOptionsso the binary safety is end-to-end. Verified with a 1024-byte payload covering 0x00..0xFF round-tripping sha256-clean through httpbin. - Node
setSessionIdentifier()no longer crashes on first call: the method was declared in JS and TypeScript but the underlyinghttpcloak_session_set_identifierentry point was missing from the koffi lib table; any user callingsession.setSessionIdentifier("foo")got an immediate runtime error. Registered. - Node
LocalProxyStatsTypeScript interface no longer fabricates fields: the.d.tsdeclaredtotalRequests / activeConnections / failedRequests / bytesSent / bytesReceivedas camelCase fields. The C-API actually emits snake_caserunning / port / active_conns / total_requests / preset / max_connections / registered_sessions, andfailedRequests / bytesSent / bytesReceivedaren't in the wire format at all. The interface now mirrors what Node code actually receives. - Node
availablePresets()TypeScript return type fixed: declaredstring[]but the runtime returnsRecord<string, { protocols: string[] }>. The type now matches reality. disable_ech/disableEchctor flag silently dropped in Python and .NET: the clibSessionConfig.DisableECHfield has accepted this since ECH shipped, but the Python ctor never passed it through and the .NET ctor didn't even have the parameter. Both bindings now exposedisable_ech/disableEchctor kwargs that wire to the JSON config.- Python
StreamResponse.cookiesexposed only 2 of 9 cookie fields:get_stream/post_stream/request_streamall built theirCookieobjects with justnameandvalue, silently droppingdomain,path,expires,max_age,secure,http_only,same_site. Stream cookies now carry the same metadata as non-stream cookies. - Node ESM
index.mjswas missing 5 named exports:PresetPool,loadPreset,loadPresetFromJSON,unregisterPreset,describePresetwere exported from the CJSindex.jsbut not re-exported from the ESM entry point. ESM consumers gotundefined. All five are now re-exported. - Python
httpcloakpackage missingCookie,RedirectInfo,StreamResponse,FileValuefrom__init__.py: these were reachable only viahttpcloak.client.X, blocking idiomatic type-hint usage andisinstancechecks. Added. - .NET FastResponse doc signatures lied about a
contentTypeparameter: the binding chapter documentedstring? contentType = nullon everyPostFast/RequestFast/PutFast/PatchFastvariant; the actual code never had it. Any user who copied the doc signature got a compile error. The doc is now honest (Content-Type goes via theheadersdictionary). - .NET ctor doc signature was missing
withoutConditionalCache: the code had the parameter, the prose example used it, but the canonical signature block in the binding chapter never listed it. Fixed. - Observability docs claimed
ClearCachewas Go-only: the table said "not exposed × 3"; reality isclear_cache()/clearCache()/ClearCache()are exposed in every binding (have been since the conditional-cache work landed). Corrected.
Added
-
Chrome 149 preset (desktop):
chrome-149plus thechrome-149-windows/-linux/-macosvariants, andchrome-latestnow tracks 149. Chrome 149's TLS and HTTP/2 fingerprint is identical to 148, verified on the wire (same JA4t13d1516h2_8daaf6152771_d8a2da3f94cdand the same Akamai H2 fingerprint), so this is a header refresh rather than a new wire shape: the User-Agent moves to149.0.0.0and thesec-ch-uabrand list rotates to"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"(Chrome reorders the brands and rolls the GREASE brand token each version, so it is not a plain version bump). The preset inherits everything else from chrome-148, and the new constants are exposed in Python, Node, and .NET. Mobile (Android / iOS) stays on 148 until a real Chrome 149 mobile capture is confirmed. -
Top-level Go convenience wrappers:
httpcloak.NewManager()(plus thehttpcloak.Manageralias forsession.Manager),httpcloak.ValidateSessionFile(path), andhttpcloak.SetKeyLogWriter(w)are re-exported at the package root, so Go callers get session-pool management, save-file validation, and TLS key-log wiring without reaching into thesessionsubpackage. -
.NET
SessionCacheBackendmanaged wrapper: distributed TLS session cache is finally reachable from .NET. ImplementISessionCache(six methods: Get/Put/Delete + GetEch/PutEch + OnError, with default implementations for the ECH pair and OnError), pass tonew SessionCacheBackend(impl), callRegister(). The wrapper pins the six callback delegates as instance fields so the GC can't collect them while Go still holds function pointers; trailing-buffer pattern frees the last-returned C string on the next callback so memory doesn't leak.IDisposable+ finalizer cleanup, single-active-backend semantics with auto-unregister-on-replace,HttpCloakCache.ConfigureSessionCache(impl)/ClearSessionCache()one-liner shorthands matching Python's idiom. End-to-end verified: TLS session ticket round-trips through an in-mem dict, 0 callbacks fire after Unregister. -
.NET binary / Stream / multipart async overloads:
PostAsync(byte[]),PostAsync(Stream),PutAsync(byte[]),PutAsync(Stream),PatchAsync(byte[]),PatchAsync(Stream),PostMultipartAsync(...),RequestBinaryAsync(method, byte[]),RequestStreamAsync(method, Stream),WarmupAsync(url, timeoutMs). The .NET binding had string-only async methods even though the sync surface had binary overloads; anyone needing theHttpClient.PostAsync(byte[])idiom had to drop to sync or convert binary to UTF-8 (lossy). All binary overloads route throughRequestBinaryAsyncwhich base64-encodes and setsbody_encoding=base64so non-UTF-8 bytes survive the cgo boundary. -
AbortSignal / asyncio.wait_for cancellation actually cancels the Go-side goroutine:
httpcloak_cancel_requesthas existed in the C-API since 2026-01 but neither Python nor Node wired it. Python's_AsyncCallbackManager.register_requestinstalls a future done-callback that callscancel_request(cid)thenunregister_callback(cid)on cancellation; Node'sAsyncCallbackManager.registerRequestaccepts an optionalAbortSignaland runs the same sequence. The order matters: cancel unblocks the goroutine viactx.Done, then unregister removes the entry from Go'sasyncCallbacksmap so the goroutine's finalinvokeCallbackfinds!existsand returns silently. Without the unregister step Node would crash withError::ThrowAsJavaScriptException napi_throwduring teardown when the late callback tried to throw into a torn-down env. Verified:asyncio.wait_for(timeout=1)andAbortController.abort()both cancel httpbin.org/delay/5 in 1.00s, no teardown crash, session usable for a fresh GET after. .NET already hadCancellationTokenwired. -
LocalProxy session enumeration (all bindings):
LocalProxy.list_sessions() / listSessions() / ListSessions()returns the IDs currently registered on the proxy;LocalProxy.has_session(id) / hasSession(id) / HasSession(id)is a cheap existence check that skips the JSON marshal. Two new C-API exports back it. Useful for operational dashboards, stale-registration GC in long-running processes, and confirming sessions actually reached the proxy. -
Session observability quartet (all bindings):
Session.stats() / Stats(),idle_time() / idleTime() / IdleTime(),is_active() / isActive() / IsActive(),touch() / Touch(). Four new C-API exports return a JSON snapshot (id, preset, created_at, last_used, request_count, active, cookie_count, cache_entry_count, age_ns, idle_time_ns, transport_stats), the idle-time in nanoseconds, an active flag, and a touch primitive that resets the idle timer without issuing a request. Long-running scrapers can finally scrape per-session metrics into Prometheus / Datadog from any binding. .NET ships a typedSessionStatsclass withCreatedAt/LastUsed(DateTimeOffset) andAge/IdleTimeSpan(TimeSpan) helper properties. -
Chunked upload (Node
uploadStream, .NETUploadStream): Python had_streaming_uploadsince the 5-entry cgo upload state machine landed; Node and .NET had no managed wrapper, so multi-GB file uploads from those bindings had to either materialise in memory or proxy throughLocalProxy. NowSession.uploadStream(method, url, chunks, options)accepts any iterable / async-iterable yieldingBuffer / Uint8Array / string(Node) orIEnumerable<byte[]>(.NET); chunks flow straight through the Go-sideio.Pipe()with no base64 envelope. Cancellation on exception callsupload_cancel. Verified with 4 × 1 KiB chunks covering 0x00..0xFF: status 200, sha256 round-trip match on both bindings. -
Preset constants refreshed across all bindings:
Preset.CHROME_LATEST/CHROME_148/CHROME_147families (plus Windows/Linux/macOS/iOS/Android platform variants),FIREFOX_LATEST/FIREFOX_148,SAFARI_LATEST/SAFARI_LATEST_IOS. 73 named constants per binding (including backwards-compat aliases), all verified resolving to real entries in the runtime registry.Preset.all()returns the same set. Backwards-compat aliases (IOS_CHROME_148,ANDROID_CHROME_LATEST,IOS_SAFARI_LATEST, etc.) keep older naming working. -
Explicit
disable_http3/disableHttp3ctor flag (all bindings): was already reachable indirectly viahttpVersion="h2"(which impliesWithDisableHTTP3on the Go side) but the explicit flag is cleaner for callers who just want "no H3" without committing to a specific lower version. cgo'sSessionConfig.DisableHTTP3is wired; ctor params added on Python (disable_http3=False), Node (disableHttp3: false), .NET (bool disableHttp3 = false). Verified all three forceprotocol=h2on a fresh request when set. -
.NET StreamResponse property surface symmetry:
StreamResponsenow exposesElapsed(TimeSpan),Encoding(charset parsed from Content-Type), andHistory(always empty for streams since the stream layer doesn't follow redirects, but the property exists for symmetry withResponseandFastResponseso callers can iterate without a null check). -
Node
availablePresets()/describePreset(name)properly typed in.d.ts:availablePresets()return type now declaresRecord<string, { protocols: string[] }>matching the runtime shape;describePreset(name): stringdeclaration added so TypeScript callers no longer need(httpcloak as any).describePreset(...)cast. -
Node binding chapter "Other exports" section rewritten: was 6 names, now 22 with one-line descriptions and cross-links:
PresetPool,Presetconstants,Cookie,RedirectInfo,describePreset,loadPreset,loadPresetFromJSON,unregisterPreset,setEchDnsServers,getEchDnsServers,availablePresets,version, plus the module-levelget/post/...convenience funcs. -
Conditional-cache control surface (all bindings): the session has always behaved like a real browser by replaying
ETagandLast-ModifiedasIf-None-Match/If-Modified-Sinceon the next request to the same URL. That's the right default for fingerprint authenticity, but callers had no way to opt out short of recreating the session. Three new controls land together:WithoutConditionalCache()SessionOption (Go),without_conditional_cache=True(Python),withoutConditionalCache: true(Node.js),withoutConditionalCache: true(.NET): disables validator injection and storage for the lifetime of the session.- Runtime mutators:
SetConditionalCacheEnabled(bool)/ConditionalCacheEnabled()(Go),set_conditional_cache(bool)/get_conditional_cache()(Python),setConditionalCache(bool)/getConditionalCache()(Node.js),SetConditionalCache(bool)/GetConditionalCache()(.NET). Toggle the same state mid-session; existing entries are preserved when paused. - Per-request opt-out via
disable_conditional_cache=True(Python) /disableConditionalCache: true(Node.js) /disableConditionalCache: true(.NET) /Request.DisableConditionalCache(Go). Wired on every request method in every binding:get/post/put/delete/patch/head/options/request/getSync/postSync/requestSync/getStream/postStream/requestStreamand the JSON / Async / sibling variants. ClearCache()(clear_cache()/clearCache()/ClearCache()) is now exposed in every binding (was Go-only).
-
Redirect runtime control (all bindings):
Session.SetFollowRedirects(bool)/FollowRedirects()andSetMaxRedirects(int)/MaxRedirects()(Go, with snake_case Python and camelCase Node / PascalCase .NET equivalents). The per-request overrideallow_redirects(Python) /allowRedirects(Node.js, .NET) /Request.FollowRedirects *bool(Go) wins over the session default for a single call. Available on every request method in every binding (same coverage asdisableConditionalCacheabove). Closes the gap that previously required recreating a session to flip redirect-following.
Changed
- .NET
SetHeaderOrder/GetHeaderOrdernow use source-gen JSON: previously called reflection-basedJsonSerializer.Serialize<string[]>(order)which breaks NativeAOT (trim warnings, runtime failures). Switched to theJsonContext.Default.StringArraysource-gen path that the rest of the binding uses.
Internal
transport.Requestandhttpcloak.RequestgainFollowRedirects *boolandDisableConditionalCache boolfields. The session-layerrequestWithRedirectshonours both before falling back tos.Config.FollowRedirectsand the session'sconditionalCacheEnabledflag.- New C entry points:
httpcloak_session_clear_cache,httpcloak_session_set_conditional_cache,httpcloak_session_get_conditional_cache,httpcloak_session_set_follow_redirects,httpcloak_session_get_follow_redirects,httpcloak_session_set_max_redirects,httpcloak_session_get_max_redirects. The clibRequestOptionsandRequestConfigJSON shapes acceptfollow_redirectsanddisable_conditional_cache. protocol.SessionConfiggainsWithoutConditionalCache bool;protocol.RequestOptionsgainsDisableConditionalCache booland the pre-existingFollowRedirects *boolfield is now actually consulted.
- Redirects now send a browser-like Referer on each hop (#70): when a session followed a redirect it carried whatever Referer was already on the request (usually none) instead of synthesizing one from the previous URL the way a browser does, so a server inspecting the Referer across a redirect chain could tell httpcloak apart from a real browser. The session now follows Chrome's default
-
v1.6.7-0.20260511081948-cdca097b92d411 May 2026 pre-releaseNothing published for this version
-
v1.6.609 May 2026Release notes
Open source →chrome-148 desktop + android presets
Adds chrome-148-windows / chrome-148-linux / chrome-148-macos /
chrome-148-android. Wire-level diff vs chrome-147 is just two
header values (User-Agent version bump + sec-ch-ua brand list
rotation). TLS extension shuffle continues per-handshake the
same way utls already produces; JA4 stays
t13d1516h2_8daaf6152771_d8a2da3f94cd. chrome-latest aliases
(and platform-specific chrome-latest-* + android-chrome-latest)
now resolve to 148. iOS already at 148 from v1.6.5.WithoutCookieJar() across all 4 bindings
New SessionOption that disables the internal cookie jar
entirely — Set-Cookie headers from responses are not stored,
the jar is not consulted to inject Cookie: headers on
subsequent requests. Caller-provided Cookie: headers always
pass through. Useful when an application maintains its own
cookie store (database, shared cache across sessions) and
wants the lib to be byte-transparent about cookies.- Go: httpcloak.WithoutCookieJar()
- Python: without_cookie_jar=True
- Node.js: withoutCookieJar: true
- .NET: withoutCookieJar: true
Guards both Request and RequestStream paths in the session
layer. Design originally proposed in andreacanes/httpcloak
(based on gkopp13's patch); implementation expanded to cover
Set-Cookie storage paths in addition to the inject-on-request
path.WithLocalAddrIP(net.IP) ergonomic alias
Drop-in net.IP-typed sibling for WithLocalAddress(string).
Lets callers who already hold a parsed IP skip the String()
round-trip. Same internal storage, nil net.IP is a no-op so
conditional option chains don't accidentally clobber a
previously-set address.Other notable
- HTTP/3 PRIORITY_UPDATE on the control stream now uses the
actual request stream ID (was hardcoded 0 — silently dropped
by H3 fingerprinters because real Chrome never references
stream 0) and the priority field value derived from the
request's "priority:" header (was hardcoded "u=0, i" — only
matched Chrome for document navigations). Visible |984832|
token now appears in h3_text matching real Chrome 147+
captures byte-for-byte. Lives in sardanioss/quic-go v1.2.25. - client.Client.DoStream cookie jar parity with Do — the
lower-level Go client API now applies jar cookies before
streaming requests AND stores Set-Cookie from streamed
responses. Session-level RequestStream and all language
bindings already had parity; this only mattered for Go users
on the lower-level client.Client API. Resolves the GH issue
asking specifically about Client.DoStream coverage. - IP_FREEBIND sockopt actually wired now when WithLocalAddress
is set. The doc comment had been claiming "Works with
IP_FREEBIND on Linux" since v1.5.x; now the kernel actually
sees IP_FREEBIND=15 / IPV6_FREEBIND=78 set on every TCP dial
socket and UDP listen socket — without it, binding to a
routed-but-not-locally-configured IPv6 from a /48 prefix
silently failed with EADDRNOTAVAIL. - @httpcloak/win32-arm64 dropped from npm optionalDependencies.
The package was never built by CI (matrix is linux-x64,
linux-arm64, darwin-x64, darwin-arm64, win32-x64) so npm
install on yarn classic / pnpm strict modes errored out at
install time. Fixed: only advertise platforms we actually
publish. Re-add when CI gets an aarch64-w64-mingw32-gcc
cross-compiler step.
Release notes
Open source →Added
- chrome-148 desktop and android presets — Adds
chrome-148-windows,chrome-148-linux,chrome-148-macos,chrome-148-androidplus theirChrome148Windows()/Chrome148Linux()/Chrome148macOS()/Chrome148()/AndroidChrome148()Go constructors. Wire-level diff vs chrome-147 is just two header values: User-Agent version bump (Chrome/147→Chrome/148) andsec-ch-uabrand list rotation (Chromium moved to first position, GREASE brand"Not.A/Brand";v="8"→"Not/A)Brand";v="99"). TLS extension shuffle continues per-handshake the same way utls already produces for chrome-147; JA4 stayst13d1516h2_8daaf6152771_d8a2da3f94cd, Akamai HTTP/2 fingerprint stays unchanged.chrome-latest/chrome-latest-windows/chrome-latest-linux/chrome-latest-macos/chrome-latest-androidaliases now resolve to 148.chrome-148-ioswas already shipped in v1.6.5. WithoutCookieJar()SessionOption (all bindings) — Disables the session's internal cookie jar entirely. When set,Set-Cookieheaders from responses are NOT stored and the jar is NOT consulted to injectCookie:headers on subsequent requests; cookie management is left fully to the caller via per-request headers. Useful when an application maintains its own cookie store (database, shared cache across sessions) and wants the lib to be byte-transparent about cookies. Caller-providedCookie:headers always pass through regardless of this option. Available across Go (httpcloak.WithoutCookieJar()), Python (without_cookie_jar=True), Node.js (withoutCookieJar: true), and .NET (withoutCookieJar: true). Guards both Request and RequestStream paths in the session layer.WithLocalAddrIP(net.IP)ergonomic alias — Drop-innet.IP-typed sibling for the existingWithLocalAddress(string)option. Lets callers who already hold a parsed IP (rotating from a precomputed pool, returned by an upstream allocator) skip theString()round-trip. Same internal storage as the string form, so mixing the two is safe; nilnet.IPis a no-op so option chains built conditionally don't accidentally clobber a previously-set address.
Fixed
@httpcloak/win32-arm64removed from npmoptionalDependencies— The package was never built by CI (onlylinux-x64,linux-arm64,darwin-x64,darwin-arm64,win32-x64are in the publish matrix), but the mainhttpcloakpackage'soptionalDependencieslisted it. npm silently skipped the missing package, so most users didn't notice; yarn classic and other strict optional-deps handlers errored out at install time even on supported platforms, blocking adoption. The phantom entry is now removed and the emptybindings/nodejs/npm/win32-arm64/directory is cleaned up. Windows-on-ARM64 users who were broken anyway now get a clearer "no matching platform binary" error at runtime instead of an "ENOENT from npm" at install. If/when CI adds anaarch64-w64-mingw32-gcccross-compiler step or a native ARM64 Windows runner, this can be re-added.- HTTP/3 PRIORITY_UPDATE on the control stream now uses the actual request stream ID and the request's priority value — Two long-standing bugs in the H3 control-stream PRIORITY_UPDATE frame, both visible only on H3 fingerprinters that parse RFC 9218 frames: (1) the
prioritized_stream_idwas hardcoded to0, which is silently dropped by H3 fingerprinters because real Chrome never emitsPRIORITY_UPDATEfor stream 0 — Chrome's 0-RTT probe burns that bidi ID, and the first real request lands on stream 4. (2) The priority field value was hardcoded to"u=0, i", only matching Chrome for document navigations. After the fix,PRIORITY_UPDATEis emitted lazily just before the first request'sHEADERSframe, with the prioritized_stream_id matching the actual stream the request is on, and the priority field value derived from the request'spriority:HTTP header (which already comes from the per-resource-type priority_table). Net wire change:h3_textnow contains the visible|984832|token betweenGREASEand the pseudo-order, matching real Chrome 147+ H3 captures byte-for-byte. Lives in thesardanioss/quic-go v1.2.25bump. client.Client.DoStreamnow applies and stores cookies via the jar — The lower-level Goclient.Clienthad cookie-jar parity onDo()since the jar shipped, butDoStreamskipped both halves: it didn't addCookie:from the jar to the request, and it didn't foldSet-Cookie:from the streamed response back into the jar. Sessions that authenticated viaDo()and then issued a streaming request silently lost their auth state on the wire, and any cookies set by streamed responses vanished. Both halves now mirror the existingDo()paths inclient.go. Session-level (session.RequestStream) and all language bindings already had parity, so this only affected Go users on the lower-levelclientAPI.- IP_FREEBIND is actually applied now when
WithLocalAddressis set — The doc comment has claimedWorks with IP_FREEBIND on Linuxsince v1.5.x, but the codebase never set the sockopt. Operators relying on the documented behaviour to bind to a routed-but-not-locally-configured IPv6 address (the documented IPv6-prefix-rotation use case) hitEADDRNOTAVAILunless they hadnet.ipv4.ip_nonlocal_bind=1set globally or ran withCAP_NET_ADMIN. Fixed: a Linux-onlyapplyFreebindhelper now setsIP_FREEBIND(15) andIPV6_FREEBIND(78) on every TCP dial socket and UDP listen socket created whenLocalAddressis non-empty. Wired into all three transports (H1/H2 direct + proxy paths, H3/QUIC UDP listen) and the SOCKS5 dialer. Non-Linux platforms get a no-op stub. Conditional gate: freebind is only applied whenLocalAddressis set, so default callers see zero behaviour change.
-
v1.6.6-0.20260509144711-976e3454f6ef09 May 2026 pre-releaseNothing published for this version
-
v1.6.530 Apr 2026Release notes
Open source →Build any browser fingerprint from JSON
describe_preset(name) emits every effective fingerprint field as a
JSON document. Mutate, load_preset_from_json, register, use. Same
workflow across Python / Node.js / .NET / Go. Round-trip byte-equal.Per-resource-type H2 stream priority (Issue #56)
Chrome 141..147 desktop/android and Firefox 148 now emit a distinct
RFC 7540 stream weight + RFC 9218 priority: header per
Sec-Fetch-Dest. Safari and iOS variants stay opted out
(NoRFC7540Priorities=true). 14-dest default table inherits when a
preset doesn't define its own.Caller-supplied headers respect HPACK position
cache-control / content-type / content-length / origin / referer /
cookie now land at their real-Chrome HPACK slot instead of being
appended after the preset's last entry.Per-request timeout uniformly seconds
Python Session.get/post/etc, .NET Session.Get/Post/etc, and Node.js
Session.get/post all accept timeout in seconds, matching
Session(timeout=). Three coordinated bugs fixed across bindings and
clib async paths.Cookie API close-out (BREAKING)
get_cookies() / getCookies() / GetCookies() now return cookie
objects with full metadata (List[Cookie] / Cookie[] / List),
closing the v1.6.1 deprecation cycle. The flat name->value dict
shape is gone. Same change for the singular get_cookie(name) /
getCookie(name) / GetCookie(name) -> Cookie object or null.
Migration: cookies.find(c => c.name == 'foo')?.value or equivalent.JSON preset loader hardening
RegisterStrict() rejects (a) name collision with a built-in,
(b) duplicate custom-name registration, (c) empty name. clib
loader paths use it. BuildPreset gains an inheritance-loop
walker (rejects based_on chains that re-enter themselves) and
early ParseJA3 validation (malformed JA3 errors at load time
instead of mid-handshake).http2.akamai shorthand authoritative override
When a custom preset spec inherits from a built-in AND sets
http2.akamai to a captured shorthand, the SETTINGS values +
WINDOW_UPDATE + stream weight + pseudo-order from the shorthand
now win over inherited discrete fields for the slots the
shorthand specifies. Previously the discrete zero defaults that
describe_preset always emits silently clobbered the captured
shorthand values.Dependency bump: sardanioss/quic-go v1.2.24
Picks up the per-connection QUIC transport parameters work
(a8287c14). Removes the long-standing local-fork replace
directive that previously madego installimpossible.Other notable
- chrome-148-ios preset + embedded JSON registry
- Expanded JSON preset spec (key_share_curves,
delegated_credential_algorithms, QUIC H3 fields) - Tweak-fingerprint examples in Python / Node.js / .NET
- WithDisableHTTP3() session option
- PresetPool rotation (round-robin / random) in all bindings
- Issue #57: retry default = 0 (was 3 in Python / Node.js)
- Issue #52: strip Referer / Authorization on scheme downgrade
and cross-origin redirects - Issue #48: Node.js FFI string leaks fixed via koffi disposable
- Issue #53: fetch_mode kwarg on every request method
- Issue #51: .NET cookie Max-Age widened to int64
- Issue #42: binary response body corruption fixed across all
bindings via base64 envelope - Hybrid-PQ JA3 handshake fix (X25519MLKEM768 first key share)
- QUIC google_connection_options corrected to ORIG
Release notes
Open source →Breaking Changes
get_cookies()/getCookies()/GetCookies()now return cookie objects with full metadata — Completes the deprecation cycle that began in v1.6.1. The flat name→value dict format is gone; the methods now return whatget_cookies_detailed()etc. used to return:List[Cookie](Python),Cookie[](Node.js),List<Cookie>(.NET). The deprecation warnings (DeprecationWarning/process.emitWarning/[Obsolete]) are removed accordingly. The same change applies to the singularget_cookie(name)/getCookie(name)/GetCookie(name), which now return a Cookie object (or null) instead of just the value string. Migration:cookies['name']→next((c.value for c in cookies if c.name == 'name'), None), or usec = s.get_cookie('name'); v = c.value if c else None.
Added
- JSON preset loader hardening —
RegisterStrict(name, preset)errors on (a) name already registered as a custom preset, (b) name collides with a shipped built-in, (c) empty name. The clib loader paths (httpcloak_preset_load_file,httpcloak_preset_load_json) now use it so user-supplied specs can no longer accidentally shadowchrome-latestor silently overwrite a previous registration.BuildPresetgains two more guards: an inheritance-loop walker (preset chains that re-enter themselves are rejected with a clear error before the build proceeds) and early JA3 format validation viaParseJA3(malformed JA3 strings now error at load time with the parser message, not as an opaque TLS handshake failure later).
Fixed
http2.akamaishorthand now authoritatively overrides inherited discrete settings — When a custom preset spec inherits from a built-in (the documenteddescribe_preset→ mutate JSON →load_preset_from_jsonworkflow) AND setshttp2.akamaito a captured shorthand, the SETTINGS values + WINDOW_UPDATE + stream weight + pseudo-order from the shorthand now win over the inherited discrete fields for the slots the shorthand specifies. Previously the discrete fields (whichdescribe_presetalways emits, including zero defaults) were applied last and overwrote the shorthand's values, so a user pasting an akamai capture would silently get the parent preset's values on the wire instead of the captured ones. Fix: parse the shorthand into a presence-aware struct (ParseAkamaiDetailedreturns which SETTINGS IDs were explicitly present), apply discrete fields first only for slots the shorthand didn't cover, then overlay shorthand values for the slots it did. Discrete fields still apply normally when no shorthand is provided.
Added
- Issue #56: Per-resource-type H2 stream priority — now the default for every RFC 7540 preset — Real browsers emit a different RFC 7540 stream weight per resource type (sec-fetch-dest), driven by an internal RFC 9218 urgency:
document/iframe/object/embed/style → u=0 (256),script/font/empty/preload-as=fetch → u=1 (220),manifest/image → u=2 (183),video/audio/track/async-defer-script → u=3 default (147),worker/prefetch/beacon → u=4 (110). The previous single-weight model emittedweight=256, exclusive=trueon every HEADERS frame regardless of dest. NewH2FingerprintConfig.PriorityTable map[string]ResourcePrioritycarries{Urgency, Incremental, EmitHeader}per dest; the deterministic formulaweight = 256 - (urgency × 73) / 2derives the H2 wire weight, andPriorityHeaderFromResourcerenders the matchingpriority:HTTP header per the four RFC 9218 emission rules. Wire-up: a new per-requestHeaderPriorityFunccallback on the underlying H2 transport (sardanioss/net v1.2.6) consults the table bySec-Fetch-Dest, returning a freshPriorityParamfor each request — same connection, different streams, distinct priorities. Resolution rule: a preset that defines its ownPriorityTableuses it as-is; a preset without one inherits a package-level default 14-dest table — but only when it uses RFC 7540 priorities (NoRFC7540Priorities=false). Safari, iOS Chrome, and iOS Safari all carryNoRFC7540Priorities=trueand stay opted out (they don't emit RFC 7540 PRIORITY frames at all). SettingPriorityTableto a non-nil empty map disables the default for a single preset. Effect on shipping presets: every chrome-* desktop/android variant (chrome-141 through chrome-147) and Firefox 148 now emit per-dest priorities by default, matching real browser behaviour. JSON spec gainspriority_tablefield on the HTTP/2 section;Describe()round-trips it byte-equal. New API surface:Preset.H2HasPriorityTable(),Preset.H2PriorityFor(dest),PriorityFromUrgency(urgency),PriorityHeaderFromResource(rp),DefaultPriorityTable(). Tests cover the formula across all 8 urgencies, every emission rule combination, full round-trip, end-to-end wire-frame capture against a local raw-framer server for every dest, default-inheritance for legacy Chrome and Firefox, NoRFC7540 opt-out for Safari/iOS variants, explicit-empty-disables override, unknown-dest fallback, per-request distinctness on a pooled connection, and concurrent request stress under-race. - User-supplied
Sec-Fetch-Dest/Sec-Fetch-Mode/Sec-Fetch-Siteare no longer clobbered by the XHR sniff — When the auto-sniff decided a request was XHR, it forcedmode=cors, dest=empty, site=cross-siteeven if the caller had explicitly pinned a different value (e.g.dest=imagefor browser sub-resource emulation). Now the sniff coercion only fills in headers the caller didn't supply; explicit pins win. Required for the priority-table architecture above to be useful — power users can now request browser sub-resource fetches like<link rel=preload as=image>,<script src>,<link rel=manifest>, etc., and get the matching wire priority. chrome-148-iospreset — New iOS Chrome 148 fingerprint with refreshed User-Agent, navigation header set, HTTP/2 wire shape, and HTTP/3 QUIC flow-control windows.chrome-latest-ios/ios-chrome-latestnow resolve to it.H3FingerprintConfig.QUICInitialStreamReceiveWindow+QUICInitialConnectionReceiveWindow— New optional pointer fields for per-preset QUIC flow-control windows. nil-default leaves quic-go defaults in place, so existing presets are unchanged. JSON spec gains matchingquic_initial_stream_receive_window/quic_initial_connection_receive_windowkeys;Describe()emits them only when set.- Chrome 147 preset family + embedded JSON registry — New
chrome-147/chrome-147-{windows,linux,macos,ios,android}presets shipped as JSON files infingerprint/embedded/and auto-registered at package init via//go:embed. All*-latestaliases now resolve to Chrome 147 via thinLookupCustomwrapper factories that delegate to the embedded JSON. The//go:embedmechanism is the future home for monthly Chrome bumps — header-only diffs ship as JSON files instead of Go-code edits. describe_preset/describePreset/Describe— flatten any preset to JSON for save / edit / reload — Newfingerprint.Describe(name)Go API plus matchinghttpcloak_describe_presetclib export and bindings (Pythondescribe_preset(name), Node.jsdescribePreset(name), .NETCustomPresets.Describe(name)). Returns a fully-resolved JSON document for any registered preset (built-in or runtime-loaded): inheritance is collapsed, getter fallbacks (H2Config/H3Confignil → Chrome defaults) are emitted explicitly, header values map keys are sorted alphabetically, andHeaderOrderslice order is preserved. The output round-trips byte-equal throughLoadPresetFromJSON→BuildPreset→Describe, so it can be saved, hand-edited, reloaded as a custom preset, and re-described without drift. Two consecutive calls return byte-identical bytes (no map-iteration leakage). Empty/zeroTCPFingerprintis omitted; theHTTP3section appears only whenSupportHTTP3=true. Unregistered utlsClientHelloIDs (e.g. randomized variants or hand-built IDs) error rather than silently corrupt JSON.JA3-defined presets dump totls.ja3+tls.ja3_extras(neverclient_hello). Verified against all 53 built-in presets in Go, Python, Node.js, and .NET — strict round-trip passes for every name inAvailable()including-latestaliases. The Node.js export uses the leak-safeHeapStrkoffi disposable from issue #48; Python uses_ptr_to_string; .NET usesNative.PtrToStringAndFree. Internal helper: newClientHelloIDName(id)inverse lookup over the canonical-name map, with concrete names taking precedence over-autoaliases (soHelloFirefox_Autoresolves tofirefox-120, not the alias).WithDisableHTTP3()session option — Disables HTTP/3 (QUIC) while keeping H1/H2 auto-negotiation. Useful when binding to a local address that doesn't support UDP or when QUIC is unreliable on the network. Previously the only way to avoid H3 wasWithForceHTTP2()which locked out H1.- JSON preset loader + custom preset registry — New
BuildPresetpath accepts a JSON spec (TLS, H2, H3, QUIC, headers, header order, TCP fingerprint) and registers named presets at runtime. Exposed viahttpcloak.loadPreset(filePath)/loadPresetFromJSON(jsonData)/unregisterPreset(name)in Python, Node.js, and .NET. Supports inheritance from built-in presets, deep-clone on lookup, mutual exclusion betweenja3+ explicit TLS fields, and PSK session resumption for JA3-defined presets. Example JSON spec files ship underexamples/presets/(Chrome 146 Linux, Safari 18, Firefox 148). PresetPoolfor rotation — Load a JSON pool file containing multiple presets and pick round-robin or random. All presets auto-register on construction; name is returned verbatim forSession(preset: ...). Available in all bindings. Hardened against nil presets, empty pools, constructor overflow, and orphaned registrations.H2FingerprintConfig/H3FingerprintConfigtypes — Explicit per-preset configuration for HTTP/2 settings, header tables, priority frames, pseudo-header order, QPACK settings, and QUIC transport parameters. Replaces hardcoded values scattered acrosshttp2_transport.go,http3_transport.go, and pool builders with preset getters. All 30 built-in presets now carry explicit H2 configs; Safari/iOS presets gained explicit H3 configs replacing the prior heuristic fallback.- Firefox 148 preset — New preset with JA3 TLS fingerprint and explicit H2/H3 configs. Illustrates the JSON preset spec with
key_share_curves,delegated_credential_algorithms, and full QUIC parameters. - Per-connection QUIC transport parameters — QUIC connection ID length and max datagram frame size are now per-connection (derived from the preset) instead of process-global constants, so mixed-preset workloads no longer leak parameters across sessions.
- Preset pool and registry exports in clib and bindings —
PresetPoollifecycle (load/pick/random/next/get/close) and the custom-preset registry are surfaced through the C API and exposed in Python/Node.js/.NET. fetchMode/fetch_modeknob on every request method — Escape hatch for requests where the auto-sniff can't pick the rightSec-Fetch-Mode. Accepts"cors","no-cors","navigate", or"websocket"and is available as a kwarg (Pythonfetch_mode), option field (Node.jsfetchMode), and parameter (.NETfetchMode:) on every Get/Post/Put/Patch/Delete/Head/Options/Request + Async/Fast/Stream variant. InjectsSec-Fetch-Mode+ a coherentSec-Fetch-Destwhen the user didn't supply them, so the final header set stays self-consistent.
Fixed
- Per-request
timeoutsemantics consistent across Python, Node.js, and .NET — Three coordinated bugs surfaced from one root cause (the clib has different unit conventions on its sync vs. async request paths): (1) PythonSession.get(url, timeout=30)routed throughSession.request()which forwarded the value as-is into the syncrequest_config.timeoutfield that the C side interprets as milliseconds, so a 30-second-intent call fast-failed in 30 ms. (2) .NETSession.Get(url, timeout: 30)had the identical issue atbindings/dotnet/HttpCloak/Session.cs:530. (3) Node.jsSession.get(url, { timeout })andSession.post(url, { timeout })never destructuredtimeoutfrom the options object, silently dropping the value; the underlying clibhttpcloak_get_async/httpcloak_post_asyncpaths parsedoptions.Timeoutbut never enforced it on the request context. Fix: PythonSession.request()and .NETSession.Request()now multiplytimeout * 1000at the boundary before stuffing the JSON config (sync C paths read ms). Node.jsget()/post()destructuretimeoutand forward asreqOptions.timeout. Clibget_async/post_asyncnow layercontext.WithTimeout(time.Second), matching the existingrequest_asyncunit. Public API across all bindings is now uniformly seconds (matchingSession(timeout=)). Verified end-to-end:s.get(url, timeout=30)returns 200 promptly;s.get(url, timeout=1)against a 2-second sleep endpoint fast-fails in ~1 second. - Caller-supplied headers landed in the wrong HPACK wire position — When a caller supplied a header outside the preset's default emit set (e.g.
cache-control: max-age=0on an F5 reload,content-typeon a POST, orcookieon a follow-up request), the magic per-requestHeader-Order:key was being populated from the preset's header values list (which only enumerates headers Chrome sends every time) instead of the full HPACK position table (which also reserves slots for situational headers). The forked H2 encoder then appended the unknown header after the last value-list entry, producing a wire ordering distinguishable from real browsers —cache-controlended up afterpriorityinstead of right after:path. Three call sites now usePreset.H2HeaderOrder()(the complete position table includingcache-control,content-type,content-length,origin,referer,cookie, andpriority):transport/transport.go:1869,client/client.go:1500,client/client.go:1585. Default fresh-nav requests stay byte-identical because the encoder skips order entries with no matchingreq.Headerkey. New regression testTestUserSuppliedCacheControl_RespectsHPACKPositionpinscache-control's wire position relative to:path/sec-ch-ua/priority. - Issue #57: Python and Node.js silently enabled 3 retries on 5xx by default — Python's
Session(retry: int = 3)and Node.js's{ retry = 3 }destructuring defaults always wroteretry=3into the session config, so callers that never asked for retries quietly fired 4 requests per failed call (1 attempt + 3 retries on the default[429, 500, 502, 503, 504]status list). Worse, this hit POST/PUT/PATCH the same as GET/HEAD — a clear idempotency violation that could double-charge or duplicate writes. Root cause was a binding-level default disagreement: .NET correctly defaulted to 0, Python and Node.js defaulted to 3. Both bindings now default to 0 (matching .NET); enabling retry is opt-in viaretry=N/{ retry: N }. Three regression locks added so this can't drift back: a Python signature test (internal_tests/python/test_retry_default.py), a Node.js source-pattern test (internal_tests/nodejs/test_retry_default.js), and a Go-level option-chain test (retry_default_test.go) that pins the default at every layer fromWithRetry/WithoutRetrydown throughNewSessionand into theprotocol.SessionConfigthat drives the retry loop. Behavior change: callers that relied on the implicit default-3 retry now see 0 retries; passretry=3explicitly for the old behavior. - JA3 with X25519MLKEM768 (group 4588) as the first supported group caused
tls: internal erroron every handshake — Firefox 141+ ships JA3s starting with4588-29-23-24-25-256-257. OurParseJA3defaultedKeyShareCurvesto 1, so the resulting spec carried a single MLKEM key share. utls' TLS 1.3 client handshake then trips itskeyShareKeys.ecdhe == nilconsistency check (handshake_client_tls13.go:63) — the preset path that generates MLKEM key shares populatesKeyShareKeys.MlkemEcdhebut not the legacyEcdhefield, while the consistency check still requiresEcdhe. The result waslocal error: tls: internal errorbefore any wire bytes left the socket. Real Firefox and Chrome always pair the MLKEM key share with an X25519 share anyway, so the fix is to auto-bumpKeyShareCurvesto 2 inParseJA3when the first non-GREASE curve is X25519MLKEM768 (0x11EC) or X25519Kyber768Draft00 (0x6399). ExplicitJA3Extras.KeyShareCurvesvalues are still honored. Added regression testsTestParseJA3_HybridPQAutoBumpsKeyShares,TestParseJA3_HybridPQRespectsExplicitKeyShareCurves, andTestParseJA3_NoBumpWithoutHybridPQ. - QUIC
google_connection_optionsregression (post-1.6.1-beta.3) — Commit7465c7e(in v1.6.1) added QUIC transport parameter 0x3128 (google_connection_options) with value"B2ON"to the Chrome H3 fingerprint. The value was wrong: in QUICHE,B2ONis the "Enable BBRv2" option, only sent by Chrome instances launched with--enable-features=QuicConnectionOptions=B2ONor a Finch override — vanishingly rare in real traffic. Stable Chrome's actual default is"ORIG"(origin-frame experiment hint). Some QUIC frontends accepted the handshake fine but silently dropped follow-up frames for non-trivial requests, manifesting as a 30sMaxIdleTimeout. Reverted the value to"ORIG"; added a transport-package regression test (TestBuildChromeTransportParams_GoogleConnectionOptions) that locks the wire bytes so this can't drift back silently. - Issue #52: Credential leakage across scheme-downgrade and cross-origin redirects — Chain
https://A → https://B → http://C → https://Dforwarded whateverRefererandAuthorizationheaders the caller set on the first hop all the way through, including to the plain-HTTP hop. Real browsers (Chrome's defaultstrict-origin-when-cross-originreferrer policy, plus WHATWG Fetch §4.3 "HTTP-redirect fetch") stripRefererentirely on anyhttps → httptransition and stripAuthorization/Proxy-Authorizationon any scheme downgrade or cross-origin redirect.curl ≥7.58does the same for auth.session.requestWithRedirectsand the parallel redirect loop inclient.Client.doOncenow both apply this scrubbing.Cookiewas already rebuilt from the cookie jar per-hop and the jar'sSecuregate was already correct — those paths are unchanged. - Issue #48: Node.js binding leaked C-allocated strings on every FFI return — Every FFI decl in
bindings/nodejs/lib/index.jsthat returned"str"let koffi copy the C string into a JS string while dropping the original pointer, which Go had allocated withC.CString(malloc). The pointer was never fed back tohttpcloak_free_string, so eachSession.get/post/request,getCookies,session.refresh, proxy getters, header-order getters, stream metadata, session save/marshal, local-proxy stats — 26 functions in all — silently leaked a few KB to tens of KB per call, producing significant RSS growth under sustained traffic. Fixed by wrapping"str"in a koffidisposabletype (HeapStr) whose auto-invoked disposer ishttpcloak_free_string, so every C→JS conversion immediately frees the source allocation. Zero call-site changes; Python and .NET already freed correctly via their own helpers and were not affected. - Issue #53: Navigate headers on bindings POST/XHR requests — The binding path (
httpcloak_post_raw→session.Do→transport.applyPresetHeaders) had an Accept-only sniff that pickedSec-Fetch-Mode: navigate,Sec-Fetch-Dest: document, andSec-Fetch-Site: nonefor any POST without an explicitAcceptheader. Python'sjson=kwarg setContent-Type: application/jsonbut notAccept, so every JSON POST emitted navigation headers — an obvious mismatch since browsers send CORS headers for fetch/XHR. The sniff now considers HTTP method,Content-Type,Accept, and any user-suppliedSec-Fetch-*headers, andapplyPresetHeadersapplies a coherent CORS header block (mode=cors, dest=empty, noupgrade-insecure-requests) when the request looks like fetch()/XHR. The direct-Go-client.Clientpath was fixed alongside the transport path so the two stay in lockstep. ExplicitSec-Fetch-Mode: navigatefrom the user still forces navigation (e.g. SPA mimicking a form submit). - Issue #51: .NET cookie
Max-Age > int32.MaxValuecrash —CookieData.MaxAge,Cookie.MaxAge, and theSetCookie(maxAge:)parameter were typed asint. Servers that advertise 100-year-lifetime cookies (Max-Age=3153600000) triggeredSystem.Text.Jsonto throw "The JSON value could not be converted to System.Int32" during deserialization, taking down sync and async request paths. All three are nowlong. Wire format unchanged; existing scripts passintliterals without change. - Issue #42: Binary response body corruption across all bindings — Non-UTF-8 response bodies (PDFs, images, gzip streams that slipped past auto-decompression) were silently corrupted when passed through the JSON response channel. The C API now base64-encodes non-UTF-8 bodies and tags them with
body_encoding: "base64"; Python, Node.js, and .NET decoders decode on receipt. Covers the main request/response path,httpcloak_upload_finish, and theSession.post()/Session.request()binary flows. - .NET and Node.js sync paths migrated to raw binary C API — The sync request paths were still routing bodies through the JSON channel, doubling binary payloads through base64 round-trips. Both now use
httpcloak_{get,post,request}_rawwhich takes(ptr, len)directly, matching Python. - Preset headers overridden by Chrome defaults at client/transport layer —
applyNavigationModeHeadersand the client layer were applying hardcoded ChromeAccept/Accept-Language/Accept-Encodingvalues on top of the preset's own headers, silently clobbering Firefox/Safari/iOS presets. Now uses preset values when present, falls back to Chrome only when the preset doesn't define that header. Pseudo-header order override in client and transport layers is also fixed —PseudoHeaderOrderfrom the preset now survives through both layers. - Pool H2 transport was missing
DisableCookieSplit: true— Pool-path HTTP/2 was sending cookies as separate HPACK entries instead of a single entry like real Chrome. Detectable by passive H2 fingerprinters. - H2 proxy CONNECT missing keep-alive — H2 proxy
CONNECTrequests did not includeConnection: keep-alive, causing some proxies to close the tunnel after the CONNECT response. - H2 transport close-race nil-map panic — Added regression tests and guards for the close-race path where a concurrent
Close()and request could dereference a nil map. LookupCustomdid not deep-clone presets — Returning a shared pointer let subsequent mutations leak across sessions. Now returns a deep copy.- Session cache guard in pool and orphaned TLS extension fields — Pool now validates TLS extension fields and guards the session cache lookup to prevent a nil-deref on certain preset shapes.
Changed
describe_presetnow emits the effectivepriority_table, including the inherited package default — PreviouslyDescribe()only emittedpriority_tablewhen the preset carried an explicit one, so a Chrome 146 dump (which inherits the 14-dest default) returned JSON that omitted the field — confusing for users who wanted to tweak just one entry, since the describe → edit → reload workflow had nothing to edit.flattenHTTP2now resolves the same way the runtime does: explicit table wins; otherwise, RFC 7540 presets emit the package default;NoRFC7540Priorities=truepresets (Safari, iOS Chrome, iOS Safari) still omit the field because they don't carry an RFC 7540 PRIORITY frame at all. EmptyPriorityTablemap is now treated identically to nil at the resolution layer (both fall through to default), simplifying the round-trip semantics. Round-trip stability locked in tests across all 50+ built-in presets.- Tweak-fingerprint examples added across Python, Node.js, and .NET — New
examples/python-examples/17_tweak_fingerprint.py,examples/js-examples/18_tweak_fingerprint.js, andexamples/csharp-examples/TweakFingerprint.csdemonstrate the four-recipe describe → edit → load workflow: bump per-resource H2 priority, customize HPACK header order, import an externally-captured JA3 + Akamai fingerprint, and clean up viaunregister_preset. README gains a flagship "Build Any Browser Fingerprint From JSON" feature section plus a compact "Custom Preset Edit Points" reference table. - JSON preset spec expanded —
key_share_curves,delegated_credential_algorithms, and QUIC H3 fields (connection_id_length,max_datagram_frame_size) are now first-class JSON fields. Inheritance, mutual exclusion validation, and deep-copy behavior are hardened in the loader. - H2 settings order is dynamic per browser type — Matches what real Chrome/Firefox/Safari send instead of a shared static order.
-
v1.6.115 Mar 2026Release notes
Open source →Added
- Chrome 146 preset — New default preset with updated
sec-ch-uabrand rotation ("Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146") and User-Agent version bump. TLS and HTTP/2 fingerprints are identical to Chrome 145/144/143. All-latestaliases now resolve to Chrome 146. All code examples updated to usechrome-latestto avoid version-specific churn. getCookiesDetailed()/getCookieDetailed()— New methods that return Cookie objects with full metadata (domain, path, expires, maxAge, secure, httpOnly, sameSite). Available in all bindings. The existinggetCookies()/getCookie()methods continue to return the old flat format (name→value dict / string) with a deprecation notice — in a future release they will return the same format as the detailed methods.- Userspace UDP receive buffering — On platforms where the kernel limits UDP socket buffer size (Azure Container Apps: 416 KiB), a dedicated drain goroutine now keeps the kernel buffer permanently drained by buffering packets in userspace (256–4096 slots). Prevents silent packet drops, retransmissions, and connection failures for HTTP/3. Activates automatically when the kernel buffer is below 7 MB; zero overhead on systems with proper buffers.
google_connection_optionsQUIC transport parameter — Chrome sendsgoogle_connection_options(0x3128) with value "B2ON" in QUIC handshakes. This was the last missing Chrome-specific transport parameter identified in a full fingerprint audit. (Note: subsequently corrected to"ORIG"in the Unreleased block — see fix entry above.)- HPACK never-indexed representation for sensitive headers —
cookie,authorization, andproxy-authorizationnow use the HPACK "Never Indexed" wire encoding (0x10 prefix) matching Chrome's behavior. Previously used "Without Indexing" (0x00 prefix) which passive H2 fingerprinters can distinguish. tcp_dfoption in Python and Node.js bindings — The DF (Don't Fragment) bit was missing from the Python and Node.js session constructors. Now all 5 TCP fingerprint fields are exposed in all bindings.- All TCP fingerprint fields in .NET binding — The .NET
Sessionconstructor andSessionConfigclass now exposetcpTtl,tcpMss,tcpWindowSize,tcpWindowScale, andtcpDfparameters.
Fixed
- Fix Cookie API losing domain/path/expiry metadata — The internal cookie jar stored full metadata correctly, but
getCookies()flattened it to a name→value dict, losing domain/path/expiry and causing last-write-wins collisions when two domains set a cookie with the same name.setCookie()now accepts domain/path/flags for domain-scoped cookies —setCookie("name", "value")still works unchanged.deleteCookie()properly removes cookies (was setting to empty string) and accepts an optional domain parameter.clearCookies()calls the Go core directly (was doing a broken client-side loop). All existing scripts continue to work —getCookies()still returns a flat dict,getCookie()still returns a string. Wire behavior, session serialization, and per-requestcookiesparameter are unchanged. - Fix pool H2 path splitting cookies per RFC 9113 — The pool
http2.Transportwas missingDisableCookieSplit: true, causing cookies to be sent as separate HPACK entries instead of a single entry like real Chrome. Detectable by passive H2 fingerprinters.
Changed
- TCP/IP fingerprint spoofing disabled by default — Spoofing (TTL, MSS, WindowSize, WindowScale, DF bit) applied to proxy connections breaks connectivity and is useless — the proxy terminates TCP, so the target never sees spoofed values. All 24 presets now ship with zero TCPFingerprint. Users can opt in via
WithTCPFingerprint()(Go) ortcp_ttl/tcp_mssetc. in bindings. - UDP buffer size warnings permanently suppressed — The
log.Printfwarnings about insufficient kernel UDP buffer sizes are removed.setReceiveBuffer/setSendBufferstill attempt to increase buffers best-effort; failures are silently handled by userspace buffering.
Dependencies
- sardanioss/utls v1.10.2 → v1.10.3
- sardanioss/quic-go v1.2.21 → v1.2.23
- sardanioss/net v1.2.4 → v1.2.5
- Chrome 146 preset — New default preset with updated
-
v1.6.1-beta.308 Mar 2026 pre-releaseRelease notes
Open source →- Fix TCP fingerprint override silently ignored (missing needsConfig check)
- Expose tcp_ttl/tcp_mss/tcp_window_size/tcp_window_scale in Python and Node.js bindings
- Add TCP/IP fingerprinting section to README
- Update CHANGELOG with all changes since 1.6.1-beta.2
- Bump version across all bindings (Python, Node.js, .NET, clib)
Release notes
Open source →Added
- TCP/IP fingerprint spoofing — Spoof OS-level TCP/IP stack parameters (TTL, MSS, Window Size, Window Scale, DF bit) to match the claimed browser platform. Anti-bot systems check SYN packet characteristics to verify Windows/Linux/macOS claims. Platform-specific presets included: Windows (TTL=128, WS=8), Linux (TTL=64, WS=7), macOS (TTL=64, WS=6). Override via
WithTCPFingerprintin Go ortcp_ttl/tcp_mss/tcp_window_size/tcp_window_scaleoptions in bindings. FetchModeNoCors— Simulate subresource loads (<script>,<link>,<img>) withsec-fetch-mode: no-corsand content-type-appropriate Accept headers. Use withFetchDestfield to setsec-fetch-dest(script, style, image).SetForceProtocol()— Switch HTTP protocol version (H1/H2/H3) at runtime without creating a new client. Useful for mimicking Chrome's H2→H3 alt-svc upgrade pattern.
Fixed
- Fix duplicate Content-Length in H1 transport — The
writeHeadersInOrder"remaining headers" loop wrote headers not in the preset order but did not mark them in the tracking map. The fallback "ensure Content-Length" block then wrote Content-Length a second time. Duplicate Content-Length is an HTTP/1.1 protocol violation — nginx and other strict servers return 400 Bad Request. This affected all H1 POST/PUT/PATCH requests with a body through all language bindings. - Fix bindings sending Navigate headers to API endpoints — The transport-level
applyPresetHeadersalways applied Navigate mode headers (sec-fetch-mode: navigate,upgrade-insecure-requests: 1) regardless of request type. API calls via Python/Node.js/.NET bindings emitted browser navigation headers on JSON requests — a clear protocol mismatch since real browsers send CORS headers for fetch/XHR. Now auto-detects CORS mode from the user's Accept header (application/json,*/*, etc.) and adjusts sec-fetch headers accordingly. - Fix Chrome 145 sending unnecessary MAX_FRAME_SIZE — Chrome omits HTTP/2 SETTINGS_MAX_FRAME_SIZE (setting 5), relying on the RFC default of 16384. Our preset was sending it explicitly, creating a fingerprint mismatch.
Changed
- H3 header order unified with H2 — Removed separate
H3HeaderOrderfrom presets. Chrome uses the samerequest_->extra_headersordered vector for both H2 and H3 (confirmed from Chromium source). The previous H3-specific order was a stale artifact from an upstream tool whose output had been observed-but-incorrectly-ordered. - QPACK Never-Index bit for sensitive headers — Cookie, Authorization, and Proxy-Authorization headers are now encoded with the N=1 (Never-Index) bit in QPACK, matching Chrome's behavior of preventing intermediaries from caching sensitive values in dynamic tables.
- H3 SETTINGS frame delivery — Re-added 5ms delay after opening control/QPACK streams to ensure the SETTINGS frame is parsed by the server before request HEADERS arrive. Without this, SETTINGS and request can be bundled in the same packet.
- Deterministic H3 header ordering — Headers not in the preset order are now sorted alphabetically instead of random Go map iteration order. Canonical key lookup added for case-insensitive header matching in QPACK encoder.
- Chrome QUIC Initial packet structure — Fixed to match Chrome's exact packet layout for fingerprint consistency.
- Chrome DefaultInitialRTT — Set to 100ms matching Chrome's PTO (Probe Timeout) behavior.
Dependencies
- quic-go v1.2.18 → v1.2.21
- qpack v0.6.2 → v0.6.3
-
v1.6.1-beta.2.0.20260227130054-d66251cfa44727 Feb 2026 pre-releaseNothing published for this version
-
v1.6.1-beta.2.0.20260227125914-65e19c2e854b27 Feb 2026 pre-releaseNothing published for this version
-
v1.6.1-beta.222 Feb 2026 pre-releaseRelease notes
Open source →Fixed
- Fix query parameters duplicated in URL for .NET async methods (
GetAsync,PostAsync) — params were applied in the method then passed again toRequestAsyncwhich applied them a second time (only affected async path with explicit timeout) - Fix
SetProxy()andSetPreset()losinginsecureSkipVerifysetting — recreated child transports started with defaultfalse, ignoring the parent'sverify: falsesetting - Fix query parameter order not preserved in .NET binding — changed
parameterstype fromDictionary<string, string>toIEnumerable<KeyValuePair<string, string>>across all request methods (source-compatible, users can now pass ordered collections likeList<KeyValuePair<>>for order-sensitive APIs)
- Fix query parameters duplicated in URL for .NET async methods (
-
v1.6.1-beta.122 Feb 2026 pre-releaseRelease notes
Open source →Added
- Custom JA3 fingerprinting — Override the preset's TLS fingerprint with a custom JA3 string. Supports all 25+ known TLS extensions, GREASE filtering, and automatic defaults for unspecified fields. Available via
WithCustomFingerprintin Go andja3option in all bindings (Python, Node.js, .NET, clib). - Custom Akamai HTTP/2 fingerprinting — Override the preset's HTTP/2 SETTINGS, WINDOW_UPDATE, PRIORITY, and pseudo-header order with an Akamai fingerprint string. Available via
WithCustomFingerprintin Go andakamaioption in all bindings. - Extra fingerprint options — Fine-tune TLS extensions beyond what JA3 captures:
tls_signature_algorithms,tls_alpn,tls_cert_compression,tls_permute_extensions. Available viaextra_fpdict in bindings orCustomFingerprintstruct fields in Go. - JA3 parser (
fingerprint/ja3.go) — Converts JA3 strings to uTLSClientHelloSpecwith extension ID toTLSExtensionmapping for 25+ known extensions, GREASE handling, and Chrome-like defaults for signature algorithms, ALPN, and cert compression. - Akamai parser (
fingerprint/akamai.go) — Converts Akamai HTTP/2 fingerprint strings toHTTP2Settings+ pseudo-header order. - JA3/Akamai unit tests — 29 unit tests covering Chrome/Firefox/Safari fingerprints, malformed input, GREASE filtering, extension type verification, defaults merging, and edge cases.
- E2E fingerprint tests — 4 E2E tests verifying JA3 match, H2 fingerprint match, preset sanity, and cross-session reproducibility.
Changed
- TLS-only mode is automatically enabled when a custom JA3 fingerprint is set (preset HTTP headers are skipped)
- Extension 50 (
signature_algorithms_cert) now uses a broader Chrome-like list includingPKCS1WithSHA1for legacy certificate chain verification - Extension 51 (
key_share) now generates a key share only for the first preferred curve, matching real browser behavior (previously generated for all curves, which was a detectable fingerprint signal)
Fixed
- Fix
DoStreammissingconfigErrcheck — invalid Akamai fingerprint errors were silently ignored for streaming requests - Fix H1 speculative TLS fallback unconditionally setting session cache — could cause handshake failures with custom JA3 specs that lack PSK extension
- Fix
ParseJA3mutating caller's*JA3Extrasstruct when filling in defaults — now makes a shallow copy - Fix
SetProxy()andSetPreset()silently dropping custom fingerprint config — recreated transports with nil config, losingCustomJA3,CustomH2Settings, speculative TLS, key log writer, and other settings - Fix
Fork()dropping custom fingerprint settings — forked sessions now copy the parent's transport config (including custom JA3, H2 settings, pseudo-header order) - Fix clib
extra_fpsilently ignored when neitherja3norakamaiis set —tls_permute_extensionsand other extra options now work standalone
- Custom JA3 fingerprinting — Override the preset's TLS fingerprint with a custom JA3 string. Supports all 25+ known TLS extensions, GREASE filtering, and automatic defaults for unspecified fields. Available via
-
v1.6.1-0.20260222004900-e58d72d39fe422 Feb 2026 pre-releaseNothing published for this version
-
v1.6.022 Feb 2026Release notes
Open source →Added
- Chrome 145 presets — Added
chrome-145,chrome-145-windows,chrome-145-linux,chrome-145-macos,chrome-145-ios,chrome-145-androidbrowser presets with updated TLS fingerprints and HTTP/2/H3 settings.
Changed
- Default preset updated from
chrome-144tochrome-145 - Total available presets increased from 18 to 24
- Chrome 145 presets — Added
-
v1.6.0-beta.13.0.20260215170735-cc4843294eb515 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.1312 Feb 2026 pre-releaseRelease notes
Open source →Added
session.Fork(n)— Create N sessions sharing cookies and TLS session caches but with independent connections. Simulates multiple browser tabs from the same browser for parallel scraping. Available in Go, Python, Node.js, and C#.session.Warmup(url)— Simulate a real browser page load by fetching HTML and all subresources (CSS, JS, images, fonts) with realistic headers, priorities, and timing. Populates TLS session tickets, cookies, and cache headers before real work begins. Available in Go, Python, Node.js, and C#.- Speculative TLS — Sends CONNECT + TLS ClientHello together on proxy connections, saving one round-trip (~25% faster proxy handshakes). Disabled by default due to compatibility issues with some proxies; enable with
enable_speculative_tls. switch_protocolon Refresh() — Switch HTTP protocol version (h1/h2/h3) when callingRefresh(), persisting for future refreshes.-latestpreset aliases —chrome-latest,firefox-latest,safari-latestaliases that automatically resolve to the newest preset version.available_presets()returns dict — Now returns a dict with protocol support info ({name: {h1, h2, h3}}) instead of a flat list.- Auto Content-Type for JSON POST — Automatically sets
Content-Type: application/jsonwhen body is a JSON object/dict. - C# CancellationToken support — Native Go context cancellation for C# async methods.
- C# Session finalizer — Prevents Go session leaks when
Dispose()is missed. disable_echtoggle — Disable ECH lookup per-session for faster first requests when ECH is not needed.cache-control: max-age=0after Refresh() — Automatically adds cache-control header to requests afterRefresh(), matching real browser F5 behavior.- Local address binding — Bind outgoing connections to a specific local IP address for IPv6 rotation. Available via
WithLocalAddressin Go andlocal_addressoption in bindings. - TLS key logging — Per-session
key_log_fileoption andSSLKEYLOGFILEenvironment variable support for Wireshark TLS inspection. - Fast-path clib bindings — Zero-copy APIs (
httpcloak_fast_*) for high-throughput transfers via C FFI. - New mobile presets — Added
chrome-144-ios,chrome-144-android,safari-18-iospresets.
Changed
- Parallel DNS + ECH resolution in SOCKS5 proxy QUIC dial path and H3 transport dial
- Pre-load x509 system root CAs at init to avoid ~40ms delay on first TLS handshake
- Default preset updated from
chrome-131/chrome-143tochrome-latest - Replace
SOCKS5UDPConnwithudpbarafor H3 proxy transport
Fixed
Transport Reliability
- Fix H2 head-of-line blocking: release
connsMuduring TCP+TLS dial so other requests aren't blocked - Fix H2 cleanup killing long-running requests by adding in-flight request counter
- Fix H2 per-address dial timeout using
min(remaining_budget/remaining_addrs, 10s) - Fix H1 POST body never sent when preset header order omits
Content-Length - Fix H1 connection returned to pool before body is fully drained
- Fix H1 deadline cleared while response body still being read
- Fix H3 UDP fallback and narrow 0-RTT early data check
- Fix H3 GREASE ID/value and QPACK capacity drift in
Refresh()/recreateTransport() - Fix H3 local address IP family filtering (IPv6 local address connecting to IPv4-only host)
- Fix H3 0-RTT rejection after
Refresh()by re-adding missing preset configurations - Fix speculative TLS causing 30s body read delay on HTTP/2 connections
- Fix speculative TLS blocklist key mismatch in H1 and H2
- Fix
bufio.Readerdata loss in proxy CONNECT for H1 and H2 - Fix corrupted pool connections, swallowed flush errors, nil-proxy guards
- Fix case-sensitive
Connectionheader, H2 cleanup race, dead MASQUE code - Fix nil-return on UDP failure and stale H2 connection entry
- Fix relative path redirect resolution using
net/urlfor proper base URL joining
Proxy & QUIC
- Fix
quic.Transportgoroutine leak in SOCKS5 H3 proxy path - Auto-cleanup proxy QUIC resources when connection dies
- Fix proxy CONNECT deadline to respect context timeout in H1 and H2
Session & Config
- Fix
verify: falsenot disabling TLS certificate validation - Fix
connect_todomain fronting connection pool key sharing - Fix POST payload encoding: use
UnsafeRelaxedJsonEscapingfor all JSON serialization - Fix per-request
X-HTTPCloak-TlsOnlyheader support in LocalProxy - Fix bogus fallback values in clib getter functions returning incorrect defaults
- Fix stale default presets (
chrome-131/chrome-143) across all bindings
Bindings
- Fix async headers not forwarded in Python
get_async()/post_async()methods - Fix clib build missing
httpcloak_fast.gosource file - Remove non-existent
chrome-131preset from all binding defaults
Resource Leaks
- Fix resource leaks and race conditions across all HTTP transports (comprehensive audit)
- Fix H3 transport
Close()blocking indefinitely on QUIC graceful drain - 8 timeout bugs fixed where context cancellation/deadline was ignored across all transports
wg.Wait()in goroutines now uses channel+select onctx.Done()time.Sleep()in goroutines replaced withselect { case <-time.After(): case <-ctx.Done(): }http.ReadResponse()on proxy connections now setsconn.SetReadDeadline()- QUIC transport
Close()wrapped incloseWithTimeout()in bothRefresh()andClose()paths
-
v1.6.0-beta.1212 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.1111 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.1011 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.911 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.811 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.710 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.610 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.510 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.408 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.308 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.207 Feb 2026 pre-releaseNothing published for this version
-
v1.6.0-beta.107 Feb 2026 pre-releaseNothing published for this version
-
v1.5.1030 Jan 2026Release notes
Open source →Baseline release. This changelog begins tracking changes from this version forward.
-
v1.5.10-0.20260122211034-8be7f529f82e22 Jan 2026 pre-releaseNothing published for this version
-
v1.5.922 Jan 2026Nothing published for this version
-
v1.5.822 Jan 2026Nothing published for this version
-
v1.5.8-0.20260119195553-65d516a801a119 Jan 2026 pre-releaseNothing published for this version
-
v1.5.717 Jan 2026Nothing published for this version
-
v1.5.7-0.20260112001546-24c4c916196d12 Jan 2026 pre-releaseNothing published for this version
-
v1.5.612 Jan 2026Nothing published for this version
-
v1.5.511 Jan 2026Nothing published for this version
-
v1.5.310 Jan 2026Nothing published for this version
-
v1.5.209 Jan 2026Nothing published for this version
-
v1.5.108 Jan 2026Nothing published for this version
-
v1.5.008 Jan 2026Nothing published for this version
-
v1.4.006 Jan 2026 withdrawnVersion retracted: Published prematurely, use v1.1.x instead
Nothing published for this version
-
v1.3.005 Jan 2026 withdrawnVersion retracted: Published prematurely, use v1.1.x instead
Nothing published for this version
-
v1.2.005 Jan 2026 withdrawnVersion retracted: Published prematurely, use v1.1.x instead
Nothing published for this version
-
v1.1.408 Jan 2026Nothing published for this version
-
v1.1.307 Jan 2026Nothing published for this version
-
v1.1.207 Jan 2026Nothing published for this version
-
v1.1.107 Jan 2026Nothing published for this version
-
v1.1.1-0.20260106102331-00efef0ebafe06 Jan 2026 pre-releaseNothing published for this version
-
v1.1.005 Jan 2026Nothing published for this version
-
v1.0.1207 Jan 2026Nothing published for this version
-
v1.0.1107 Jan 2026Nothing published for this version