PackageTrack
Sign in Get early access

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 2026
2026
Release Pre-release Withdrawn

Releases

latest 60 of 85
  1. v1.6.11 16 Aug 2026
    Release notes

    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.

    Open source →
    Release notes

    Added

    • Request.OnRedirect decides 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, the Referer policy, the cookie jar and the credential scrubbing that make following a redirect correct in the first place. The Request types in the root, client and transport packages now carry an OnRedirect func(*Redirect) error that is called once per hop before the follow-up request is built. Return nil to follow it, ErrUseLastResponse to 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 an errors.Is against your own sentinel matches what you returned. The Redirect it 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: a Set-Cookie or 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 on strings.Contains(To, "example.com"), which also passes for https://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 keeps Authorization from following a redirect off-origin. It is not called for a 3xx with no Location, 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, matching Response.GetHeader / Response.GetHeaders; use the plural for Set-Cookie, which legitimately repeats.

    • Request.GetBody re-opens a body that has to go out twice: needed only when the body is a genuine stream. For *bytes.Reader, *bytes.Buffer and *strings.Reader one is derived automatically at no cost, since the bytes are already in memory. It returns io.Reader rather than net/http's io.ReadCloser on purpose: the value is handed to http.NewRequestWithContext, and it is that function's type switch on the concrete reader type that sets Content-Length. An io.NopCloser wrapper 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.Request carries a body in two fields, Body []byte and BodyReader io.Reader, and Session.Do populates only the second, because the public Request.Body is an io.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's Content-Type still 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 from GetBody, carries GetBody onto the hop so a second 307 in the same chain also works, and refuses the hop with ErrBodyNotReplayable when 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 its Location and drive the rest themselves. Sending a request the caller believes carries a body, without the body, is not a thing to do quietly. client.Client never 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-Length with 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 the Content-Length it 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, DisableHighEntropyClientHints and the per-request Timeout were silently discarded after the first hop, and a per-request FollowRedirects: &true against 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. The Timeout is a clamp rather than an extension: one overall deadline for the whole chain is still established at the first hop, and context.WithTimeout keeps 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 with errors.Is nor see where the chain had got to. It is now ErrTooManyRedirects, and the session hands back the response carrying the last Location alongside it, the way net/http does when CheckRedirect fails. Nothing is leaked by ignoring it: the body is already buffered, so closing it is a no-op. The client package 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.

    Open source →
  2. v1.6.11-0.20260814171422-133bb5c729b9 14 Aug 2026 pre-release

    Nothing published for this version

  3. v1.6.11-0.20260814170403-91f3d4c4335c 14 Aug 2026 pre-release

    Nothing published for this version

  4. v1.6.10 14 Aug 2026
    Release notes

    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.

    Open source →
    Release notes

    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 and chrome-latest-ios deliberately still resolves to the confirmed 150 profile until a capture lands.

    • LocalProxy.CreateClient() and LocalProxy.CreateFingerprintHandler() on .NET: building a client from the proxy now actually fingerprints https:// 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, and CreateFingerprintHandler() 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-133 and firefox-148 now ship -windows, -linux and -macos variants, with matching firefox-latest-* aliases, so you can pin Firefox-on-Windows from a Linux host without hand-overriding the User-Agent. The plain firefox-133 / firefox-148 names 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.HeaderOrder sets the header order for a single request, without touching the session: SetHeaderOrder is 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. The Request types in the root, client, and transport packages now carry a HeaderOrder field 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 match SetHeaderOrder exactly, 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.Client API 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 the client package 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 SetHeaderOrder now 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 passing nil or an empty list still resets to the preset default.

    • Response.Location() resolves the redirect target the way net/http does: the standard library's http.Response has a Location() method that parses the Location header into a *url.URL and resolves it against the request URL, so a relative /login comes back as the full https://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. The Response types in the root, client, and transport packages 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's ErrNoLocation sentinel when no Location header is present, matching net/http semantics 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.

    Open source →
  5. v1.6.9 14 Aug 2026 withdrawn

    Version retracted: Tagged from a pre-fix commit: TLS verification fails open. Use v1.6.10.

    Release notes

    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.

    Open source →
  6. v1.6.8 12 Jul 2026
    Release notes

    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): HttpCloakHandler routed its requests differently from Session, 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).

    Open source →
  7. v1.6.8-beta.1.0.20260708073506-223b1acf094c 08 Jul 2026 pre-release

    Nothing published for this version

  8. v1.6.8-beta.1.0.20260605183152-165b7124f3d5 05 Jun 2026 pre-release

    Nothing published for this version

  9. v1.6.8-beta.1 05 Jun 2026 pre-release
    Release notes

    chore(release): bump version to 1.6.8-beta.1, cut CHANGELOG [1.6.8-be…

    Open source →
    Release notes

    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-CH for things like sec-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 the User-Agent and the always-on sec-ch-ua reported 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 with sec-ch-ua and the User-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 any sec-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 every sec-ch-* header except the ones you set yourself, and a softer one that keeps the always-on sec-ch-ua / sec-ch-ua-mobile / sec-ch-ua-platform trio 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.
    Open source →
  10. v1.6.7 03 Jun 2026
    Release notes

    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-origin policy 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 example https://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 / DoWithBody silently dropped the per-request Request.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 parameter rejections 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 / disableEch on 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=3 line on H2/H3. They split it into one cookie field 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-supplied Cookie header 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_async ran multipart through body_bytes.decode("latin-1") (lossy on the JSON-encoding side) and raw bytes through data.decode("utf-8") (raised on any non-UTF-8 byte); Node's post() / request() async did body.toString("utf8") on any Buffer body. Both bindings now base64-encode binary payloads and set body_encoding="base64" on the request config so the cgo boundary preserves every byte. Sync paths and text bodies are unchanged. cgo's post_async entry point gained a matching body_encoding field on RequestOptions so 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 underlying httpcloak_session_set_identifier entry point was missing from the koffi lib table; any user calling session.setSessionIdentifier("foo") got an immediate runtime error. Registered.
    • Node LocalProxyStats TypeScript interface no longer fabricates fields: the .d.ts declared totalRequests / activeConnections / failedRequests / bytesSent / bytesReceived as camelCase fields. The C-API actually emits snake_case running / port / active_conns / total_requests / preset / max_connections / registered_sessions, and failedRequests / bytesSent / bytesReceived aren't in the wire format at all. The interface now mirrors what Node code actually receives.
    • Node availablePresets() TypeScript return type fixed: declared string[] but the runtime returns Record<string, { protocols: string[] }>. The type now matches reality.
    • disable_ech / disableEch ctor flag silently dropped in Python and .NET: the clib SessionConfig.DisableECH field 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 expose disable_ech / disableEch ctor kwargs that wire to the JSON config.
    • Python StreamResponse.cookies exposed only 2 of 9 cookie fields: get_stream / post_stream / request_stream all built their Cookie objects with just name and value, silently dropping domain, path, expires, max_age, secure, http_only, same_site. Stream cookies now carry the same metadata as non-stream cookies.
    • Node ESM index.mjs was missing 5 named exports: PresetPool, loadPreset, loadPresetFromJSON, unregisterPreset, describePreset were exported from the CJS index.js but not re-exported from the ESM entry point. ESM consumers got undefined. All five are now re-exported.
    • Python httpcloak package missing Cookie, RedirectInfo, StreamResponse, FileValue from __init__.py: these were reachable only via httpcloak.client.X, blocking idiomatic type-hint usage and isinstance checks. Added.
    • .NET FastResponse doc signatures lied about a contentType parameter: the binding chapter documented string? contentType = null on every PostFast / RequestFast / PutFast / PatchFast variant; 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 the headers dictionary).
    • .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 ClearCache was Go-only: the table said "not exposed × 3"; reality is clear_cache() / clearCache() / ClearCache() are exposed in every binding (have been since the conditional-cache work landed). Corrected.

    Added

    • Chrome 149 preset (desktop): chrome-149 plus the chrome-149-windows / -linux / -macos variants, and chrome-latest now tracks 149. Chrome 149's TLS and HTTP/2 fingerprint is identical to 148, verified on the wire (same JA4 t13d1516h2_8daaf6152771_d8a2da3f94cd and the same Akamai H2 fingerprint), so this is a header refresh rather than a new wire shape: the User-Agent moves to 149.0.0.0 and the sec-ch-ua brand 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 the httpcloak.Manager alias for session.Manager), httpcloak.ValidateSessionFile(path), and httpcloak.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 the session subpackage.

    • .NET SessionCacheBackend managed wrapper: distributed TLS session cache is finally reachable from .NET. Implement ISessionCache (six methods: Get/Put/Delete + GetEch/PutEch + OnError, with default implementations for the ECH pair and OnError), pass to new SessionCacheBackend(impl), call Register(). 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 the HttpClient.PostAsync(byte[]) idiom had to drop to sync or convert binary to UTF-8 (lossy). All binary overloads route through RequestBinaryAsync which base64-encodes and sets body_encoding=base64 so non-UTF-8 bytes survive the cgo boundary.

    • AbortSignal / asyncio.wait_for cancellation actually cancels the Go-side goroutine: httpcloak_cancel_request has existed in the C-API since 2026-01 but neither Python nor Node wired it. Python's _AsyncCallbackManager.register_request installs a future done-callback that calls cancel_request(cid) then unregister_callback(cid) on cancellation; Node's AsyncCallbackManager.registerRequest accepts an optional AbortSignal and runs the same sequence. The order matters: cancel unblocks the goroutine via ctx.Done, then unregister removes the entry from Go's asyncCallbacks map so the goroutine's final invokeCallback finds !exists and returns silently. Without the unregister step Node would crash with Error::ThrowAsJavaScriptException napi_throw during teardown when the late callback tried to throw into a torn-down env. Verified: asyncio.wait_for(timeout=1) and AbortController.abort() both cancel httpbin.org/delay/5 in 1.00s, no teardown crash, session usable for a fresh GET after. .NET already had CancellationToken wired.

    • 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 typed SessionStats class with CreatedAt / LastUsed (DateTimeOffset) and Age / IdleTimeSpan (TimeSpan) helper properties.

    • Chunked upload (Node uploadStream, .NET UploadStream): Python had _streaming_upload since 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 through LocalProxy. Now Session.uploadStream(method, url, chunks, options) accepts any iterable / async-iterable yielding Buffer / Uint8Array / string (Node) or IEnumerable<byte[]> (.NET); chunks flow straight through the Go-side io.Pipe() with no base64 envelope. Cancellation on exception calls upload_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_147 families (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 / disableHttp3 ctor flag (all bindings): was already reachable indirectly via httpVersion="h2" (which implies WithDisableHTTP3 on the Go side) but the explicit flag is cleaner for callers who just want "no H3" without committing to a specific lower version. cgo's SessionConfig.DisableHTTP3 is wired; ctor params added on Python (disable_http3=False), Node (disableHttp3: false), .NET (bool disableHttp3 = false). Verified all three force protocol=h2 on a fresh request when set.

    • .NET StreamResponse property surface symmetry: StreamResponse now exposes Elapsed (TimeSpan), Encoding (charset parsed from Content-Type), and History (always empty for streams since the stream layer doesn't follow redirects, but the property exists for symmetry with Response and FastResponse so callers can iterate without a null check).

    • Node availablePresets() / describePreset(name) properly typed in .d.ts: availablePresets() return type now declares Record<string, { protocols: string[] }> matching the runtime shape; describePreset(name): string declaration 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, Preset constants, Cookie, RedirectInfo, describePreset, loadPreset, loadPresetFromJSON, unregisterPreset, setEchDnsServers, getEchDnsServers, availablePresets, version, plus the module-level get/post/... convenience funcs.

    • Conditional-cache control surface (all bindings): the session has always behaved like a real browser by replaying ETag and Last-Modified as If-None-Match / If-Modified-Since on 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 / requestStream and 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() and SetMaxRedirects(int) / MaxRedirects() (Go, with snake_case Python and camelCase Node / PascalCase .NET equivalents). The per-request override allow_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 as disableConditionalCache above). Closes the gap that previously required recreating a session to flip redirect-following.

    Changed

    • .NET SetHeaderOrder / GetHeaderOrder now use source-gen JSON: previously called reflection-based JsonSerializer.Serialize<string[]>(order) which breaks NativeAOT (trim warnings, runtime failures). Switched to the JsonContext.Default.StringArray source-gen path that the rest of the binding uses.

    Internal

    • transport.Request and httpcloak.Request gain FollowRedirects *bool and DisableConditionalCache bool fields. The session-layer requestWithRedirects honours both before falling back to s.Config.FollowRedirects and the session's conditionalCacheEnabled flag.
    • 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 clib RequestOptions and RequestConfig JSON shapes accept follow_redirects and disable_conditional_cache.
    • protocol.SessionConfig gains WithoutConditionalCache bool; protocol.RequestOptions gains DisableConditionalCache bool and the pre-existing FollowRedirects *bool field is now actually consulted.
    Open source →
  11. v1.6.7-0.20260511081948-cdca097b92d4 11 May 2026 pre-release

    Nothing published for this version

  12. v1.6.6 09 May 2026
    Release notes

    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.
    Open source →
    Release notes

    Added

    • chrome-148 desktop and android presets — Adds chrome-148-windows, chrome-148-linux, chrome-148-macos, chrome-148-android plus their Chrome148Windows() / Chrome148Linux() / Chrome148macOS() / Chrome148() / AndroidChrome148() Go constructors. Wire-level diff vs chrome-147 is just two header values: User-Agent version bump (Chrome/147Chrome/148) and sec-ch-ua brand 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 stays t13d1516h2_8daaf6152771_d8a2da3f94cd, Akamai HTTP/2 fingerprint stays unchanged. chrome-latest / chrome-latest-windows / chrome-latest-linux / chrome-latest-macos / chrome-latest-android aliases now resolve to 148. chrome-148-ios was already shipped in v1.6.5.
    • WithoutCookieJar() SessionOption (all bindings) — Disables the session's internal cookie jar entirely. When set, Set-Cookie headers from responses are NOT stored and the jar is NOT consulted to inject Cookie: 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-provided Cookie: 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-in net.IP-typed sibling for the existing WithLocalAddress(string) option. Lets callers who already hold a parsed IP (rotating from a precomputed pool, returned by an upstream allocator) skip the String() round-trip. Same internal storage as the string form, so mixing the two is safe; nil net.IP is a no-op so option chains built conditionally don't accidentally clobber a previously-set address.

    Fixed

    • @httpcloak/win32-arm64 removed from npm optionalDependencies — The package was never built by CI (only linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64 are in the publish matrix), but the main httpcloak package's optionalDependencies listed 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 empty bindings/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 an aarch64-w64-mingw32-gcc cross-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_id was hardcoded to 0, which is silently dropped by H3 fingerprinters because real Chrome never emits PRIORITY_UPDATE for 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_UPDATE is emitted lazily just before the first request's HEADERS frame, with the prioritized_stream_id matching the actual stream the request is on, and the priority field value derived from the request's priority: HTTP header (which already comes from the per-resource-type priority_table). Net wire change: h3_text now contains the visible |984832| token between GREASE and the pseudo-order, matching real Chrome 147+ H3 captures byte-for-byte. Lives in the sardanioss/quic-go v1.2.25 bump.
    • client.Client.DoStream now applies and stores cookies via the jar — The lower-level Go client.Client had cookie-jar parity on Do() since the jar shipped, but DoStream skipped both halves: it didn't add Cookie: from the jar to the request, and it didn't fold Set-Cookie: from the streamed response back into the jar. Sessions that authenticated via Do() 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 existing Do() paths in client.go. Session-level (session.RequestStream) and all language bindings already had parity, so this only affected Go users on the lower-level client API.
    • IP_FREEBIND is actually applied now when WithLocalAddress is set — The doc comment has claimed Works with IP_FREEBIND on Linux since 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) hit EADDRNOTAVAIL unless they had net.ipv4.ip_nonlocal_bind=1 set globally or ran with CAP_NET_ADMIN. Fixed: a Linux-only applyFreebind helper now sets IP_FREEBIND (15) and IPV6_FREEBIND (78) on every TCP dial socket and UDP listen socket created when LocalAddress is 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 when LocalAddress is set, so default callers see zero behaviour change.
    Open source →
  13. v1.6.6-0.20260509144711-976e3454f6ef 09 May 2026 pre-release

    Nothing published for this version

  14. v1.6.5 30 Apr 2026
    Release notes

    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 made go install impossible.

    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
    Open source →
    Release notes

    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 what get_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 singular get_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 use c = s.get_cookie('name'); v = c.value if c else None.

    Added

    • JSON preset loader hardeningRegisterStrict(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 shadow chrome-latest or silently overwrite a previous registration. BuildPreset gains 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 via ParseJA3 (malformed JA3 strings now error at load time with the parser message, not as an opaque TLS handshake failure later).

    Fixed

    • http2.akamai shorthand now authoritatively overrides inherited discrete settings — When a custom preset spec inherits from a built-in (the documented describe_preset → mutate JSON → load_preset_from_json workflow) AND sets http2.akamai to 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 (which describe_preset always 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 (ParseAkamaiDetailed returns 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 emitted weight=256, exclusive=true on every HEADERS frame regardless of dest. New H2FingerprintConfig.PriorityTable map[string]ResourcePriority carries {Urgency, Incremental, EmitHeader} per dest; the deterministic formula weight = 256 - (urgency × 73) / 2 derives the H2 wire weight, and PriorityHeaderFromResource renders the matching priority: HTTP header per the four RFC 9218 emission rules. Wire-up: a new per-request HeaderPriorityFunc callback on the underlying H2 transport (sardanioss/net v1.2.6) consults the table by Sec-Fetch-Dest, returning a fresh PriorityParam for each request — same connection, different streams, distinct priorities. Resolution rule: a preset that defines its own PriorityTable uses 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 carry NoRFC7540Priorities=true and stay opted out (they don't emit RFC 7540 PRIORITY frames at all). Setting PriorityTable to 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 gains priority_table field 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-Site are no longer clobbered by the XHR sniff — When the auto-sniff decided a request was XHR, it forced mode=cors, dest=empty, site=cross-site even if the caller had explicitly pinned a different value (e.g. dest=image for 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-ios preset — 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-latest now 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 matching quic_initial_stream_receive_window / quic_initial_connection_receive_window keys; 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 in fingerprint/embedded/ and auto-registered at package init via //go:embed. All *-latest aliases now resolve to Chrome 147 via thin LookupCustom wrapper factories that delegate to the embedded JSON. The //go:embed mechanism 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 — New fingerprint.Describe(name) Go API plus matching httpcloak_describe_preset clib export and bindings (Python describe_preset(name), Node.js describePreset(name), .NET CustomPresets.Describe(name)). Returns a fully-resolved JSON document for any registered preset (built-in or runtime-loaded): inheritance is collapsed, getter fallbacks (H2Config / H3Config nil → Chrome defaults) are emitted explicitly, header values map keys are sorted alphabetically, and HeaderOrder slice order is preserved. The output round-trips byte-equal through LoadPresetFromJSONBuildPresetDescribe, 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/zero TCPFingerprint is omitted; the HTTP3 section appears only when SupportHTTP3=true. Unregistered utls ClientHelloIDs (e.g. randomized variants or hand-built IDs) error rather than silently corrupt JSON. JA3-defined presets dump to tls.ja3 + tls.ja3_extras (never client_hello). Verified against all 53 built-in presets in Go, Python, Node.js, and .NET — strict round-trip passes for every name in Available() including -latest aliases. The Node.js export uses the leak-safe HeapStr koffi disposable from issue #48; Python uses _ptr_to_string; .NET uses Native.PtrToStringAndFree. Internal helper: new ClientHelloIDName(id) inverse lookup over the canonical-name map, with concrete names taking precedence over -auto aliases (so HelloFirefox_Auto resolves to firefox-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 was WithForceHTTP2() which locked out H1.
    • JSON preset loader + custom preset registry — New BuildPreset path accepts a JSON spec (TLS, H2, H3, QUIC, headers, header order, TCP fingerprint) and registers named presets at runtime. Exposed via httpcloak.loadPreset(filePath) / loadPresetFromJSON(jsonData) / unregisterPreset(name) in Python, Node.js, and .NET. Supports inheritance from built-in presets, deep-clone on lookup, mutual exclusion between ja3 + explicit TLS fields, and PSK session resumption for JA3-defined presets. Example JSON spec files ship under examples/presets/ (Chrome 146 Linux, Safari 18, Firefox 148).
    • PresetPool for 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 for Session(preset: ...). Available in all bindings. Hardened against nil presets, empty pools, constructor overflow, and orphaned registrations.
    • H2FingerprintConfig / H3FingerprintConfig types — 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 across http2_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 bindingsPresetPool lifecycle (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_mode knob on every request method — Escape hatch for requests where the auto-sniff can't pick the right Sec-Fetch-Mode. Accepts "cors", "no-cors", "navigate", or "websocket" and is available as a kwarg (Python fetch_mode), option field (Node.js fetchMode), and parameter (.NET fetchMode:) on every Get/Post/Put/Patch/Delete/Head/Options/Request + Async/Fast/Stream variant. Injects Sec-Fetch-Mode + a coherent Sec-Fetch-Dest when the user didn't supply them, so the final header set stays self-consistent.

    Fixed

    • Per-request timeout semantics 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) Python Session.get(url, timeout=30) routed through Session.request() which forwarded the value as-is into the sync request_config.timeout field that the C side interprets as milliseconds, so a 30-second-intent call fast-failed in 30 ms. (2) .NET Session.Get(url, timeout: 30) had the identical issue at bindings/dotnet/HttpCloak/Session.cs:530. (3) Node.js Session.get(url, { timeout }) and Session.post(url, { timeout }) never destructured timeout from the options object, silently dropping the value; the underlying clib httpcloak_get_async / httpcloak_post_async paths parsed options.Timeout but never enforced it on the request context. Fix: Python Session.request() and .NET Session.Request() now multiply timeout * 1000 at the boundary before stuffing the JSON config (sync C paths read ms). Node.js get() / post() destructure timeout and forward as reqOptions.timeout. Clib get_async / post_async now layer context.WithTimeout(time.Second), matching the existing request_async unit. Public API across all bindings is now uniformly seconds (matching Session(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=0 on an F5 reload, content-type on a POST, or cookie on a follow-up request), the magic per-request Header-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-control ended up after priority instead of right after :path. Three call sites now use Preset.H2HeaderOrder() (the complete position table including cache-control, content-type, content-length, origin, referer, cookie, and priority): 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 matching req.Header key. New regression test TestUserSuppliedCacheControl_RespectsHPACKPosition pins cache-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 wrote retry=3 into 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 via retry=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 from WithRetry / WithoutRetry down through NewSession and into the protocol.SessionConfig that drives the retry loop. Behavior change: callers that relied on the implicit default-3 retry now see 0 retries; pass retry=3 explicitly for the old behavior.
    • JA3 with X25519MLKEM768 (group 4588) as the first supported group caused tls: internal error on every handshake — Firefox 141+ ships JA3s starting with 4588-29-23-24-25-256-257. Our ParseJA3 defaulted KeyShareCurves to 1, so the resulting spec carried a single MLKEM key share. utls' TLS 1.3 client handshake then trips its keyShareKeys.ecdhe == nil consistency check (handshake_client_tls13.go:63) — the preset path that generates MLKEM key shares populates KeyShareKeys.MlkemEcdhe but not the legacy Ecdhe field, while the consistency check still requires Ecdhe. The result was local error: tls: internal error before 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-bump KeyShareCurves to 2 in ParseJA3 when the first non-GREASE curve is X25519MLKEM768 (0x11EC) or X25519Kyber768Draft00 (0x6399). Explicit JA3Extras.KeyShareCurves values are still honored. Added regression tests TestParseJA3_HybridPQAutoBumpsKeyShares, TestParseJA3_HybridPQRespectsExplicitKeyShareCurves, and TestParseJA3_NoBumpWithoutHybridPQ.
    • QUIC google_connection_options regression (post-1.6.1-beta.3) — Commit 7465c7e (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, B2ON is the "Enable BBRv2" option, only sent by Chrome instances launched with --enable-features=QuicConnectionOptions=B2ON or 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 30s MaxIdleTimeout. 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://D forwarded whatever Referer and Authorization headers the caller set on the first hop all the way through, including to the plain-HTTP hop. Real browsers (Chrome's default strict-origin-when-cross-origin referrer policy, plus WHATWG Fetch §4.3 "HTTP-redirect fetch") strip Referer entirely on any https → http transition and strip Authorization / Proxy-Authorization on any scheme downgrade or cross-origin redirect. curl ≥7.58 does the same for auth. session.requestWithRedirects and the parallel redirect loop in client.Client.doOnce now both apply this scrubbing. Cookie was already rebuilt from the cookie jar per-hop and the jar's Secure gate 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.js that returned "str" let koffi copy the C string into a JS string while dropping the original pointer, which Go had allocated with C.CString (malloc). The pointer was never fed back to httpcloak_free_string, so each Session.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 koffi disposable type (HeapStr) whose auto-invoked disposer is httpcloak_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_rawsession.Dotransport.applyPresetHeaders) had an Accept-only sniff that picked Sec-Fetch-Mode: navigate, Sec-Fetch-Dest: document, and Sec-Fetch-Site: none for any POST without an explicit Accept header. Python's json= kwarg set Content-Type: application/json but not Accept, 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-supplied Sec-Fetch-* headers, and applyPresetHeaders applies a coherent CORS header block (mode=cors, dest=empty, no upgrade-insecure-requests) when the request looks like fetch()/XHR. The direct-Go-client.Client path was fixed alongside the transport path so the two stay in lockstep. Explicit Sec-Fetch-Mode: navigate from the user still forces navigation (e.g. SPA mimicking a form submit).
    • Issue #51: .NET cookie Max-Age > int32.MaxValue crashCookieData.MaxAge, Cookie.MaxAge, and the SetCookie(maxAge:) parameter were typed as int. Servers that advertise 100-year-lifetime cookies (Max-Age=3153600000) triggered System.Text.Json to throw "The JSON value could not be converted to System.Int32" during deserialization, taking down sync and async request paths. All three are now long. Wire format unchanged; existing scripts pass int literals 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 the Session.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}_raw which takes (ptr, len) directly, matching Python.
    • Preset headers overridden by Chrome defaults at client/transport layerapplyNavigationModeHeaders and the client layer were applying hardcoded Chrome Accept / Accept-Language / Accept-Encoding values 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 — PseudoHeaderOrder from 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 CONNECT requests did not include Connection: 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.
    • LookupCustom did 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_preset now emits the effective priority_table, including the inherited package default — Previously Describe() only emitted priority_table when 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. flattenHTTP2 now resolves the same way the runtime does: explicit table wins; otherwise, RFC 7540 presets emit the package default; NoRFC7540Priorities=true presets (Safari, iOS Chrome, iOS Safari) still omit the field because they don't carry an RFC 7540 PRIORITY frame at all. Empty PriorityTable map 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, and examples/csharp-examples/TweakFingerprint.cs demonstrate 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 via unregister_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 expandedkey_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.
    Open source →
  15. v1.6.1 15 Mar 2026
    Release notes

    Added

    • Chrome 146 preset — New default preset with updated sec-ch-ua brand 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 -latest aliases now resolve to Chrome 146. All code examples updated to use chrome-latest to 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 existing getCookies() / 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_options QUIC transport parameter — Chrome sends google_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 headerscookie, authorization, and proxy-authorization now 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_df option 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 Session constructor and SessionConfig class now expose tcpTtl, tcpMss, tcpWindowSize, tcpWindowScale, and tcpDf parameters.

    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-request cookies parameter are unchanged.
    • Fix pool H2 path splitting cookies per RFC 9113 — The pool http2.Transport was missing DisableCookieSplit: 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) or tcp_ttl/tcp_mss etc. in bindings.
    • UDP buffer size warnings permanently suppressed — The log.Printf warnings about insufficient kernel UDP buffer sizes are removed. setReceiveBuffer/setSendBuffer still 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
    Open source →
  16. v1.6.1-beta.3 08 Mar 2026 pre-release
    Release notes
    • 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)
    Open source →
    Release notes

    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 WithTCPFingerprint in Go or tcp_ttl/tcp_mss/tcp_window_size/tcp_window_scale options in bindings.
    • FetchModeNoCors — Simulate subresource loads (<script>, <link>, <img>) with sec-fetch-mode: no-cors and content-type-appropriate Accept headers. Use with FetchDest field to set sec-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 applyPresetHeaders always 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 H3HeaderOrder from presets. Chrome uses the same request_->extra_headers ordered 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
    Open source →
  17. v1.6.1-beta.2.0.20260227130054-d66251cfa447 27 Feb 2026 pre-release

    Nothing published for this version

  18. v1.6.1-beta.2.0.20260227125914-65e19c2e854b 27 Feb 2026 pre-release

    Nothing published for this version

  19. v1.6.1-beta.2 22 Feb 2026 pre-release
    Release notes

    Fixed

    • Fix query parameters duplicated in URL for .NET async methods (GetAsync, PostAsync) — params were applied in the method then passed again to RequestAsync which applied them a second time (only affected async path with explicit timeout)
    • Fix SetProxy() and SetPreset() losing insecureSkipVerify setting — recreated child transports started with default false, ignoring the parent's verify: false setting
    • Fix query parameter order not preserved in .NET binding — changed parameters type from Dictionary<string, string> to IEnumerable<KeyValuePair<string, string>> across all request methods (source-compatible, users can now pass ordered collections like List<KeyValuePair<>> for order-sensitive APIs)
    Open source →
  20. v1.6.1-beta.1 22 Feb 2026 pre-release
    Release notes

    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 WithCustomFingerprint in Go and ja3 option 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 WithCustomFingerprint in Go and akamai option 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 via extra_fp dict in bindings or CustomFingerprint struct fields in Go.
    • JA3 parser (fingerprint/ja3.go) — Converts JA3 strings to uTLS ClientHelloSpec with extension ID to TLSExtension mapping 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 to HTTP2Settings + 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 including PKCS1WithSHA1 for 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 DoStream missing configErr check — 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 ParseJA3 mutating caller's *JA3Extras struct when filling in defaults — now makes a shallow copy
    • Fix SetProxy() and SetPreset() silently dropping custom fingerprint config — recreated transports with nil config, losing CustomJA3, 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_fp silently ignored when neither ja3 nor akamai is set — tls_permute_extensions and other extra options now work standalone
    Open source →
  21. v1.6.1-0.20260222004900-e58d72d39fe4 22 Feb 2026 pre-release

    Nothing published for this version

  22. v1.6.0 22 Feb 2026
    Release notes

    Added

    • Chrome 145 presets — Added chrome-145, chrome-145-windows, chrome-145-linux, chrome-145-macos, chrome-145-ios, chrome-145-android browser presets with updated TLS fingerprints and HTTP/2/H3 settings.

    Changed

    • Default preset updated from chrome-144 to chrome-145
    • Total available presets increased from 18 to 24
    Open source →
  23. v1.6.0-beta.13.0.20260215170735-cc4843294eb5 15 Feb 2026 pre-release

    Nothing published for this version

  24. v1.6.0-beta.13 12 Feb 2026 pre-release
    Release notes

    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_protocol on Refresh() — Switch HTTP protocol version (h1/h2/h3) when calling Refresh(), persisting for future refreshes.
    • -latest preset aliaseschrome-latest, firefox-latest, safari-latest aliases 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/json when 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_ech toggle — Disable ECH lookup per-session for faster first requests when ECH is not needed.
    • cache-control: max-age=0 after Refresh() — Automatically adds cache-control header to requests after Refresh(), matching real browser F5 behavior.
    • Local address binding — Bind outgoing connections to a specific local IP address for IPv6 rotation. Available via WithLocalAddress in Go and local_address option in bindings.
    • TLS key logging — Per-session key_log_file option and SSLKEYLOGFILE environment 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-ios presets.

    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-143 to chrome-latest
    • Replace SOCKS5UDPConn with udpbara for H3 proxy transport

    Fixed

    Transport Reliability

    • Fix H2 head-of-line blocking: release connsMu during 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.Reader data loss in proxy CONNECT for H1 and H2
    • Fix corrupted pool connections, swallowed flush errors, nil-proxy guards
    • Fix case-sensitive Connection header, H2 cleanup race, dead MASQUE code
    • Fix nil-return on UDP failure and stale H2 connection entry
    • Fix relative path redirect resolution using net/url for proper base URL joining

    Proxy & QUIC

    • Fix quic.Transport goroutine 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: false not disabling TLS certificate validation
    • Fix connect_to domain fronting connection pool key sharing
    • Fix POST payload encoding: use UnsafeRelaxedJsonEscaping for all JSON serialization
    • Fix per-request X-HTTPCloak-TlsOnly header 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.go source file
    • Remove non-existent chrome-131 preset 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 on ctx.Done()
    • time.Sleep() in goroutines replaced with select { case <-time.After(): case <-ctx.Done(): }
    • http.ReadResponse() on proxy connections now sets conn.SetReadDeadline()
    • QUIC transport Close() wrapped in closeWithTimeout() in both Refresh() and Close() paths
    Open source →
  25. v1.6.0-beta.12 12 Feb 2026 pre-release

    Nothing published for this version

  26. v1.6.0-beta.11 11 Feb 2026 pre-release

    Nothing published for this version

  27. v1.6.0-beta.10 11 Feb 2026 pre-release

    Nothing published for this version

  28. v1.6.0-beta.9 11 Feb 2026 pre-release

    Nothing published for this version

  29. v1.6.0-beta.8 11 Feb 2026 pre-release

    Nothing published for this version

  30. v1.6.0-beta.7 10 Feb 2026 pre-release

    Nothing published for this version

  31. v1.6.0-beta.6 10 Feb 2026 pre-release

    Nothing published for this version

  32. v1.6.0-beta.5 10 Feb 2026 pre-release

    Nothing published for this version

  33. v1.6.0-beta.4 08 Feb 2026 pre-release

    Nothing published for this version

  34. v1.6.0-beta.3 08 Feb 2026 pre-release

    Nothing published for this version

  35. v1.6.0-beta.2 07 Feb 2026 pre-release

    Nothing published for this version

  36. v1.6.0-beta.1 07 Feb 2026 pre-release

    Nothing published for this version

  37. v1.5.10 30 Jan 2026
    Release notes

    Baseline release. This changelog begins tracking changes from this version forward.

    Open source →
  38. v1.5.10-0.20260122211034-8be7f529f82e 22 Jan 2026 pre-release

    Nothing published for this version

  39. v1.5.9 22 Jan 2026

    Nothing published for this version

  40. v1.5.8 22 Jan 2026

    Nothing published for this version

  41. v1.5.8-0.20260119195553-65d516a801a1 19 Jan 2026 pre-release

    Nothing published for this version

  42. v1.5.7 17 Jan 2026

    Nothing published for this version

  43. v1.5.7-0.20260112001546-24c4c916196d 12 Jan 2026 pre-release

    Nothing published for this version

  44. v1.5.6 12 Jan 2026

    Nothing published for this version

  45. v1.5.5 11 Jan 2026

    Nothing published for this version

  46. v1.5.3 10 Jan 2026

    Nothing published for this version

  47. v1.5.2 09 Jan 2026

    Nothing published for this version

  48. v1.5.1 08 Jan 2026

    Nothing published for this version

  49. v1.5.0 08 Jan 2026

    Nothing published for this version

  50. v1.4.0 06 Jan 2026 withdrawn

    Version retracted: Published prematurely, use v1.1.x instead

    Nothing published for this version

  51. v1.3.0 05 Jan 2026 withdrawn

    Version retracted: Published prematurely, use v1.1.x instead

    Nothing published for this version

  52. v1.2.0 05 Jan 2026 withdrawn

    Version retracted: Published prematurely, use v1.1.x instead

    Nothing published for this version

  53. v1.1.4 08 Jan 2026

    Nothing published for this version

  54. v1.1.3 07 Jan 2026

    Nothing published for this version

  55. v1.1.2 07 Jan 2026

    Nothing published for this version

  56. v1.1.1 07 Jan 2026

    Nothing published for this version

  57. v1.1.1-0.20260106102331-00efef0ebafe 06 Jan 2026 pre-release

    Nothing published for this version

  58. v1.1.0 05 Jan 2026

    Nothing published for this version

  59. v1.0.12 07 Jan 2026

    Nothing published for this version

  60. v1.0.11 07 Jan 2026

    Nothing published for this version

Every package, every release, already written down.

The archive is open and free. Watching your own project is what we are building next.

Browse the archive