lkrequest 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lkrequest-0.2.0 → lkrequest-0.2.2}/.gitignore +1 -0
- lkrequest-0.2.2/CHANGELOG.md +138 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/Cargo.lock +242 -379
- {lkrequest-0.2.0 → lkrequest-0.2.2}/Cargo.toml +23 -1
- {lkrequest-0.2.0 → lkrequest-0.2.2}/PKG-INFO +211 -9
- {lkrequest-0.2.0 → lkrequest-0.2.2}/README-zh.md +191 -8
- {lkrequest-0.2.0 → lkrequest-0.2.2}/README.md +210 -8
- {lkrequest-0.2.0 → lkrequest-0.2.2}/benchmarks/bench_latency.py +6 -2
- {lkrequest-0.2.0 → lkrequest-0.2.2}/benchmarks/bench_memory.py +4 -2
- {lkrequest-0.2.0 → lkrequest-0.2.2}/benchmarks/bench_throughput.py +4 -2
- {lkrequest-0.2.0 → lkrequest-0.2.2}/benchmarks/requirements.txt +1 -2
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/browser_fingerprints.py +1 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/cookie_management.py +34 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/error_handling.py +17 -1
- lkrequest-0.2.2/examples/ip_family.py +148 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/response_inspection.py +11 -0
- lkrequest-0.2.2/examples/streaming_upload.py +128 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/justfile +4 -4
- {lkrequest-0.2.0 → lkrequest-0.2.2}/pyproject.toml +3 -1
- {lkrequest-0.2.0 → lkrequest-0.2.2}/python/lkrequest/__init__.py +23 -1
- {lkrequest-0.2.0 → lkrequest-0.2.2}/python/lkrequest/_lkrequest.pyi +562 -21
- {lkrequest-0.2.0 → lkrequest-0.2.2}/python/lkrequest/blocking.py +8 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/client.rs +286 -11
- lkrequest-0.2.2/src/dns.rs +135 -0
- lkrequest-0.2.2/src/error.rs +158 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/fingerprint.rs +126 -3
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/lib.rs +16 -0
- lkrequest-0.2.2/src/masque.rs +224 -0
- lkrequest-0.2.2/src/network_partition.rs +212 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/protocol.rs +8 -8
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/proxy.rs +65 -11
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/quic.rs +31 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/response.rs +140 -18
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/session.rs +283 -18
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/types.rs +100 -4
- lkrequest-0.2.2/src/upload.rs +336 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/websocket.rs +6 -2
- lkrequest-0.2.2/tests/_masque_proxy.py +219 -0
- lkrequest-0.2.2/tests/conftest.py +501 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/tests/test_advanced.py +416 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/tests/test_client.py +141 -4
- lkrequest-0.2.2/tests/test_followup_features.py +917 -0
- lkrequest-0.2.2/tests/test_local_server.py +1401 -0
- lkrequest-0.2.2/tests/test_masque.py +289 -0
- lkrequest-0.2.0/CHANGELOG.md +0 -62
- lkrequest-0.2.0/src/error.rs +0 -48
- lkrequest-0.2.0/tests/conftest.py +0 -234
- lkrequest-0.2.0/tests/test_followup_features.py +0 -450
- lkrequest-0.2.0/tests/test_local_server.py +0 -413
- {lkrequest-0.2.0 → lkrequest-0.2.2}/.editorconfig +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/.github/workflows/ci.yml +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/CONTRIBUTING.md +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/LICENSE +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/NOTICE +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/SECURITY.md +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/benchmarks/README.md +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/build.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/accept_encoding.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/authentication.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/basic_requests.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/certificate_config.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/client_pool.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/connection_prewarming.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/custom_fingerprint.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/event_hooks.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/fingerprint_validation.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/http_methods.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/logging_config.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/metrics.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/middleware.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/multipart_upload.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/pool_stats.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/proxy_pool.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/redirect_history.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/retry_strategies.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/session_pool.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/streaming_response.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/timeout_config.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/examples/websocket.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/python/lkrequest/pcap.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/rustfmt.toml +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/bridge.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/client_pool.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/hsts.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/metrics.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/middleware.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/multipart.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/priority.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/randomize.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/retry.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/src/session_pool.rs +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/tests/test_package_metadata.py +0 -0
- {lkrequest-0.2.0 → lkrequest-0.2.2}/tests/test_stub_parity.py +0 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All important changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## lkrequest-py 0.2.2
|
|
6
|
+
|
|
7
|
+
Adds streaming uploads, the Chrome 153, Chrome 154 and Firefox 156 fingerprints,
|
|
8
|
+
experimental MASQUE (CONNECT-UDP) proxies and address-family control with dial
|
|
9
|
+
target overrides, makes `text()` usable on the non-UTF-8 web, and picks up three
|
|
10
|
+
upstream fixes: a connection-pool bug that made every request re-dial whenever
|
|
11
|
+
HTTP/3 fell back to HTTP/2, a QPACK bug that made HTTP/3 connections built from
|
|
12
|
+
the Chrome presets unusable after their first request against servers such as
|
|
13
|
+
LiteSpeed, and HTTP/3 never reaching an IPv6 address. Six behaviour changes are
|
|
14
|
+
worth reading before upgrading: what `text()` does with bytes it cannot decode,
|
|
15
|
+
the exception `json()` raises, the order of the `Cookie` header, the QUIC GREASE
|
|
16
|
+
fields the Chromium presets now randomize per connection, the default port for a
|
|
17
|
+
portless HTTP proxy URL, and the encoding of `ProxyConfig.identity()`.
|
|
18
|
+
|
|
19
|
+
### 🚀 Features
|
|
20
|
+
- **Streaming Uploads** — Added `body_stream=` and `content_length=` to `post`, `put`, `patch`, `delete`, `request` and `send_streaming` on both clients, uploading without holding the body in memory. `body_stream` accepts a file-like object (`read(n)`), any iterable of `bytes`, or — on the async client — an async iterable; the blocking client rejects an async iterable up front, since it has no event loop to drive it. Omit `content_length` for an unknown-length body: HTTP/1.1 then uses chunked framing, HTTP/2 and HTTP/3 send DATA frames with no `Content-Length`. A supplied length is exact, so a source that ends short or runs long fails the request. Chunk boundaries are passed through untouched rather than re-chunked, because they are visible on the wire as DATA frame sizes. The source is single-use: neither an automatic retry, a protocol fallback, nor a redirect that must preserve the body can replay it.
|
|
21
|
+
- **HTTP/2 Dispatch Batching** — Added `Client(h2_dispatch_batch_size=...)` and a matching read-only property, capping how many ready HTTP/2 requests are coalesced into one driver write turn. The default of 1 keeps each request in its own write, which is the shape the browser presets were captured with; batching is faster but observable, so it is opt-in. `0` is rejected with `ValueError`.
|
|
22
|
+
- **Readable Cookie Attributes** — Added `session.get_cookies_with_attrs(url)` and `session.get_all_cookies()` on both clients, returning `Cookie` objects carrying `name`, `value`, `domain`, `host_only`, `path`, `secure`, `http_only`, `same_site`, `expires` and `is_persistent`. `get_cookies()` returns `(name, value)` pairs, which cannot tell apart two cookies sharing a name at different paths — the jar holds both, and only the attributes say which is which. These are what a caller needs to hand a jar to a browser automation context, to persist a login and know which cookies are session-only, and to answer why a cookie the jar holds was not sent. `get_cookies_with_attrs` is scoped to a URL like `get_cookies`; `get_all_cookies` spans every domain, which is the view to persist. `Cookie` is hashable, so two jars can be diffed with set operations. `get_all_cookies` lists cookies oldest first, which is the order to write them back in: combined with the `Cookie` ordering fix below, a jar saved and restored reproduces the same headers on the wire.
|
|
23
|
+
- **Address Family Selection** — Added `Client(ip_family="any" | "ipv4" | "ipv6")` on both clients, backed by upstream's address-family policy. On a dual-stack host the OS resolver orders IPv6 ahead of IPv4 (RFC 6724), so a target publishing an AAAA record is reached over IPv6 whenever IPv6 happens to work; `"ipv4"` or `"ipv6"` keeps every address this process dials — the target, the proxy's own address, a SOCKS5 UDP relay, QUIC — within one family, which is what keeps a session from leaving over two families and presenting two source addresses to the target. `"ipv6"` also refuses IPv4-mapped addresses (`::ffff:a.b.c.d`). The default `"any"` is the previous behaviour. An address outside the family raises an error that names the `ip_family` setting (`LkConnectionError`, or `ProxyError` for the proxy's own address) instead of falling back — not even to a direct connection under `proxy_fallback_direct=True` — and an unknown value raises `ValueError`. Composes with `dns=` and with `system_dns_cache_ttl=`. A proxy that resolves the target itself — HTTP `CONNECT`, `socks5h` or MASQUE handed a hostname — still picks the family it dials; `connect_to` hands it an address instead.
|
|
24
|
+
- **Dial Target Overrides** — Added `Client(connect_to={"host:port": "ip:port"})` on both clients: requests to that origin dial the given address, while the URL, TLS SNI and certificate name, `Host`, cookies and connection pooling keep the original host. It applies to direct, proxied and HTTP/3 connections, ahead of Alt-Svc / SVCB endpoints, and never to the proxy's own address; through an HTTP `CONNECT` proxy the `CONNECT` line carries the address, which is how to decide the family a proxy dials. An IPv6 host or address goes in brackets (`"[::1]:8443"`, `"[2001:db8::1]:443"`). The mapping is fixed — nothing re-resolves or refreshes it. A malformed entry, a zero port, one origin listed twice (`Example.com` and `example.com` count as the same), or an address `ip_family` excludes raises `ValueError` when the client is built.
|
|
25
|
+
- **Chrome 153 Fingerprint** — Added `Client.chrome_153()` / `BlockingClient.chrome_153()`, the `"chrome_153"` name for `tls_profile=` / `h2_profile=` / `quic_profile=`, and `TlsProfile.chrome_153()`, `TlsProfile.chrome_153_quic()`, `H2Profile.chrome_153()`, `QuicProfile.chrome_153()`. Captured from Chrome 153.0.8010.37 on Windows. Against Chrome 152 the TLS shape is the same except for the shuffled trust-anchor list (extension `0xca34`), which narrows from 32 identifiers to 28; H2 is unchanged; the QUIC ClientHello keeps the nine classic signature algorithms and sends no TLS GREASE slots. Nothing switches over on its own — an existing client keeps the preset it names.
|
|
26
|
+
- **Chrome 154 And Firefox 156 Fingerprints** — Added `Client.chrome_154()` / `BlockingClient.chrome_154()`, the `"chrome_154"` name for `tls_profile=` / `h2_profile=` / `quic_profile=`, and `TlsProfile.chrome_154()`, `TlsProfile.chrome_154_quic()`, `H2Profile.chrome_154()`, `QuicProfile.chrome_154()`. Captured from Chrome 154.0.8037.58 on Windows: the TCP and QUIC ClientHellos, H2, the QUIC transport parameters and the Initial-packet generation rules all match Chrome 153, so only the profile name differs. The capture never received a QUIC response, so the H3 SETTINGS and request priority carry over from Chrome 153's completed navigation instead of being observed anew. Added `Client.firefox_156()` / `BlockingClient.firefox_156()`, the `"firefox_156"` name for `tls_profile=` / `h2_profile=`, and `TlsProfile.firefox_156()`, `H2Profile.firefox_156()`. Captured from Firefox 156.0.1 on Windows: against Firefox 147 the TLS profile drops two ECDSA CBC suites (`0xc009`, `0xc00a`) and both FFDHE groups (256, 257), and its ECH GREASE switches to AES-128-GCM with a fixed 240-byte payload; H2 is unchanged. Like the other Firefox presets it carries no QUIC profile and turns HTTP/3 off — upstream captured Firefox 156's QUIC Initial but cannot yet reproduce every field of it — so there is no `"firefox_156"` QUIC name. As before, an existing client keeps the preset it names.
|
|
27
|
+
- **Chromium GREASE Generation For The QUIC Presets** — inherited from upstream, and it changes the wire for existing presets, not just Chrome 153. Three fields are now generated per connection the way Chromium generates them, instead of being fixed by the profile: the reserved HTTP/3 SETTINGS id and value plus the control-stream GREASE frame (every Chrome `QuicProfile`, the generic `chrome` included); the reserved QUIC transport parameter ids, now spanning the full 62-bit range with 0–15 byte payloads and a randomized GREASE version position; and the Initial packet layout, with CRYPTO data split and shuffled and PING/PADDING interleaved (both on Chrome 146 and 150–154). Two connections from one client therefore no longer look byte-identical in those fields — which is what real Chrome does, and the reason a fixed value was a fingerprint of its own. A `QuicProfile` loaded from JSON is unaffected: the three flags default to off, so a serialized or hand-built profile keeps its previous behaviour.
|
|
28
|
+
- **MASQUE Proxies (Experimental)** — **experimental upstream and here**: `MasqueConfig`, the `masque=` / `tunnel_probe=` parameters and the behaviour of `masque://` routes may change in any release without a deprecation period, and interoperability has been checked only against test proxies, not against independent production MASQUE deployments. Behind the new `masque` build feature, which implies `quic-h3` and is compiled into the published wheels; it does nothing unless a `masque://` proxy is used. A `masque://` proxy (port 443 by default) carries the QUIC connection to the target inside an RFC 9298 CONNECT-UDP tunnel over an HTTP/3 connection to the proxy; the target is named in the tunnel request, so DNS resolves proxy-side as with `socks5h`. Added `MasqueConfig` for the hop to the proxy — `server_name`, `ca_cert` (PEM file) / `ca_cert_pem`, `use_native_certs`, `verify`, and the two tunnel-lifecycle knobs `tunnel_idle_timeout` (default 120 s, `None` never retires an idle tunnel) and `max_idle_tunnels` (default 256, `0` removes the cap) — passed as `Client(masque=...)` on both clients. Its trust settings are deliberately independent of the client's `verify` / `ca_cert*`, which govern the origin: relaxing one hop for debugging never relaxes the other. A PEM bundle that is malformed or holds no certificate raises `ValueError` instead of leaving the trust store unchanged. `HealthCheckConfig` gained `tunnel_probe` and `masque`: a TCP connect proves nothing about a MASQUE proxy, so by default the health check skips those proxies — neither credited nor blacklisted — and with `tunnel_probe=True` it opens a real CONNECT-UDP tunnel to the check's target instead. Upstream skips the probe with only a log line when no outer configuration is given; here both arrive in one call, so `tunnel_probe=True` without `masque=` raises `ValueError` rather than silently never probing. MASQUE carries UDP only: HTTP/1.1 and HTTP/2 requests through it raise `ProxyError`, a failed tunnel never falls back to a direct connection even with `proxy_fallback_direct=True`, and a MASQUE proxy cannot be a chain hop. The connection to the proxy is not a browser fingerprint (the tunneled one to the origin is), and DNS-discovered ECH configs and H3 hints are unavailable on this route.
|
|
29
|
+
- **Proxy Credentials Beyond Username/Password** — Added `ProxyConfig.with_http_auth(scheme, credentials)` for a static HTTP credential sent as-is (`Bearer`, `Preshared`, …), `ProxyConfig.with_user_pass(username, password)`, and `ProxyConfig.with_auth_header("proxy-authorization" | "authorization")` to choose the header it travels in: RFC 9298 leaves proxy authentication unspecified, and MASQUE deployments disagree on the field. Each returns a new config and replaces any credential already set. An HTTP credential on a SOCKS5 proxy, which has nowhere to carry it, raises `ValueError`. Works for HTTP `CONNECT` proxies as well as MASQUE.
|
|
30
|
+
|
|
31
|
+
### 🐞 Fixes
|
|
32
|
+
- **The `Cookie` Header Follows Chrome's Order** — inherited from upstream, and visible on the wire. The jar iterated a hash map, so the `Cookie` header order was arbitrary and differed between two sessions holding the same cookies; `cookie_order` pinned only the names it listed, and the relative order of same-name cookies at different paths stayed random. Cookies are now sorted as Chrome's `CookieMonster` sorts them: longer paths first, then older cookies first. Re-setting a cookie to the same value keeps its age, a new value makes it a new cookie, and removed or expired cookies do not disturb the order. This also makes the documented order of `get_cookie()`, `get_cookies()` and `get_cookie_values()` (longest path first) actually true — it was not. Upstream verified the result against Chrome 153.0.8010.47 on the wire: the same `Set-Cookie` sequence produced an identical `Cookie` header from both.
|
|
33
|
+
- **Enums Are Hashable Again** — `HttpVersion`, `HttpIntent`, `PreferredHttpVersion`, `Idempotency`, `BrokenQuicPolicy` and `WsMessage` raised `TypeError: unhashable type` when used as a dict key or put in a set, which is the obvious way to map a member onto something. They declared equality without a matching hash, and CPython answers that by setting `__hash__ = None`. Equality is unchanged, including the integer comparisons (`HttpVersion.H2 == 2`). Still unhashable, pending a `Hash` derive upstream: `Layers`, `NegotiabilityFloor`, `H2DataFramePolicy`, `TlsSessionResumptionPolicy`, `TlsSessionCachePartitionPolicy` and `NetworkPartitionContext`.
|
|
34
|
+
- **`set_cookie_raw` On The Blocking Session** — the blocking `Session` was missing it, although the async one had it and upstream's blocking session supported it all along. Since `set_cookie_with_attrs` accepts neither `Expires` nor `SameSite`, blocking callers had no way to set those two attributes at all.
|
|
35
|
+
- **`text()` Decodes The Declared Charset** — `Response.text()` decoded UTF-8 unconditionally, so a `charset=gbk` body — or Big5, Shift_JIS, EUC-KR, any of the legacy web — was unreachable through it even though `encoding` reported the charset correctly. The failure was also misleading: it surfaced as `TypeError: function takes exactly 5 arguments (1 given)`, because the error path constructed a `UnicodeDecodeError` from a single string and that exception requires five. `text()` now decodes with the charset from `Content-Type`, resolved against the WHATWG Encoding Standard as a browser resolves it, falling back to UTF-8 when none is declared or the label is unusable. `text(encoding=...)` overrides it for a charset the server omitted or got wrong; that argument is looked up in Python's own codec registry, so Python spellings the web standard does not list (`latin-1`, `utf-8-sig`, `cp936`) work as written, and an unknown codec raises `LookupError` just as `bytes.decode` would. Bytes that cannot be decoded become U+FFFD instead of raising, matching `requests` and `httpx` — reach for `content` when the exact bytes matter. A BOM is stripped and, where it contradicts the declared charset, believed over it. That is the browser rule and it goes further than `requests` and `httpx`, which strip nothing and always let the header win: stripping keeps an invisible U+FEFF from silently defeating `json.loads` and `startswith` (`requests.json()` fails outright on a BOM-prefixed UTF-8 body), and believing the BOM is almost always right, since a UTF-8 BOM read as the declared charset is garbage no real document opens with. The visible consequence is that a `charset=gbk` response whose body starts with a UTF-8 BOM decodes as UTF-8. The streaming responses' `text()` (`StreamingResponse` and `BlockingStreamingResponse`) now decodes the same way and takes the same `encoding=` argument; it used to decode strict UTF-8 whatever the header said, and raised `RequestError` on any other body.
|
|
36
|
+
- **Exceptions Can Cross A Process Boundary** — every exception class failed to pickle, so a worker in a `ProcessPoolExecutor`, `multiprocessing` pool or task queue could not report one to its parent: the real error was replaced by `PicklingError: Can't pickle <class '_lkrequest.TimeoutError'>: import of module '_lkrequest' failed`, losing the message and no longer matching `except lkrequest.RequestError`. `pickle` stores a class by name — it imports `__module__` and looks up `__qualname__` there — and the classes named `_lkrequest`, which is not importable on its own, since the extension lives inside the `lkrequest` package. They now report `__module__ = "lkrequest"`, which also makes `repr()` show the path callers actually import from. One class had a second, independent problem: `ProxyError` reported itself as `LkProxyError`, a name the package does not export, so the lookup failed even once the module resolved. Names are now pinned to the exported ones by a test.
|
|
37
|
+
- **`json()` Failures Are Now `RequestError`s** — a body that is not JSON raised the stdlib `json.JSONDecodeError`, which is not a `RequestError`, so `except lkrequest.RequestError` missed it and retry logic needed a separate case for it. It now raises `lkrequest.JsonDecodeError`, which subclasses **both** `RequestError` and `json.JSONDecodeError`, so either `except` catches it and existing code written against the stdlib exception keeps working. It carries the stdlib exception's `msg`, `doc`, `pos`, `lineno` and `colno`.
|
|
38
|
+
- **HTTP/3 Fallback No Longer Loses The Connection Pool** — with HTTP/3 enabled (the default), an origin whose QUIC was unreachable re-established TCP and TLS for *every* request: the fallback pooled its HTTP/2 connection, but the pool lookup for `Http3WithFallback` only ever searched for an HTTP/3 connection and never saw it. There was no error, only latency — roughly a 3× slowdown against affected origins, which include any host advertising Alt-Svc `h3` from a network where UDP is blocked.
|
|
39
|
+
- **HTTP/3 Connections Survive The QPACK Dynamic Table** — inherited from upstream. Every Chrome `QuicProfile`, the generic `chrome` included, advertises a 64 KiB QPACK dynamic table as Chrome does, but the client could decode only static-table references. Against a server whose encoder takes up that offer — LiteSpeed does, and so does anything built on ls-qpack — the first request on an HTTP/3 connection succeeded and every later one failed with `QPACK_DECOMPRESSION_FAILED`, taking the connection down each time, so HTTP/3 connections were never reused. The client now builds the dynamic table from the server's encoder stream, holds a response until the entries it references have arrived, and acknowledges on the QPACK decoder stream as Chrome does. The SETTINGS on the wire are unchanged.
|
|
40
|
+
- **HTTP/3 Reaches IPv6 Addresses** — inherited from upstream. The QUIC endpoint was bound to the IPv4 wildcard only, so every HTTP/3 connection to an IPv6 address failed before a packet was sent: with `http3_only=True` the request raised, and under the default policy it fell back to HTTP/2 and marked the origin's QUIC broken. On a network with IPv6, a dual-stack site whose AAAA record sorted first therefore never got HTTP/3. The client now keeps a separate IPv6 endpoint.
|
|
41
|
+
- **Portless HTTP Proxy URLs Default To Port 80** — `proxy="http://proxy.example.com"` previously defaulted to **8080**, which is a common proxy port but not the scheme's default. It now resolves to 80, as the URL says. Spell the port out (`http://proxy.example.com:8080`) if 8080 was what you relied on. `https://` proxies are unaffected (443 either way).
|
|
42
|
+
- **`ProxyConfig.identity()` Encoding Changed** — the pooling and failure-tracking key is now a JSON array of ordered hops, each carrying protocol, host, port and a credential fragment, replacing the `user@host:port` / `a>b>c` form. The old encoding could not tell `socks5` from `socks5h`, so two chains that resolve hostnames at different hops shared one cooldown entry. The fragment is `null` without credentials, `"user:<name>"` for a username/password (never the password), and an opaque fingerprint for an HTTP credential, so two tokens on one gateway do not share a key. Treat the value as an opaque key; use `str(config)` for something human-readable.
|
|
43
|
+
- **Direct TCP Connections Disable Nagle By Default** — interactive HTTP benefits from `TCP_NODELAY`, so it is now on unless a fingerprint sets it off explicitly (`TcpFingerprint(tcp_nodelay=False)`).
|
|
44
|
+
- **Streaming Responses Honour Request Middleware And HSTS** — `send_streaming` skipped both. Request middleware now runs in the same order as on the buffered path, followed by the session's HSTS URL upgrade, and the response updates HSTS state. Full-body response middleware still does not run, since there is no full body to hand it.
|
|
45
|
+
- **Replay Safety Across A Protocol Switch** — a request that changed protocol mid-flight could be retried even though the earlier attempt may already have reached the server. Retry eligibility is now tracked through the switch.
|
|
46
|
+
- **Faster HTTP/2 Request Processing** — upstream reworked the HTTP/2 driver's write path and HPACK encoder. No API change, and the default dispatch batch size keeps the wire shape identical.
|
|
47
|
+
|
|
48
|
+
## lkrequest-py 0.2.1
|
|
49
|
+
|
|
50
|
+
Adds the Chrome 152 preset and browser network session policies, and picks up a
|
|
51
|
+
large upstream RFC-compliance pass. Several fixes below change observable
|
|
52
|
+
behaviour — the ECH, retry, redirect, cookie and connection-limit entries are
|
|
53
|
+
the ones most likely to affect existing code.
|
|
54
|
+
|
|
55
|
+
### 🚀 Features
|
|
56
|
+
- **Chrome 152 Preset** — Added `Client.chrome_152()`, `BlockingClient.chrome_152()`, `TlsProfile.chrome_152()`, `TlsProfile.chrome_152_quic()`, `H2Profile.chrome_152()`, `QuicProfile.chrome_152()`, and the `"chrome_152"` TLS/H2/QUIC profile names. Chrome 152 is currently the only preset that applies GREASE to `signature_algorithms`, and it carries the new trust anchor IDs extension. HTTP/2 is unchanged from Chrome 151; the QUIC capture drops the `google_initial_rtt` transport parameter (`0x3127`).
|
|
57
|
+
- **Browser Network Session Policies** — Added `TlsSessionResumptionPolicy` (`BROWSER_DEFAULT` / `DISABLED` / `enabled(max_tickets_per_server)`), `TlsSessionCachePartitionPolicy` (`UNPARTITIONED` / `TOP_LEVEL_SITE` / `TOP_LEVEL_AND_FRAME_SITE` / `CHROMIUM` / `FIREFOX`), and `NetworkPartitionContext(top_level_site, frame_site)`. Set the first two via `Client(tls_session_resumption_policy=..., tls_session_cache_partition_policy=...)` and pass the third to `session(network_partition_context=...)` to reproduce a browser's per-site ticket isolation. These control whether tickets are *stored* and how the cache is keyed; `SessionResumptionConfig` continues to control which resumption modes the ClientHello *advertises*.
|
|
58
|
+
- **HTTP/2 DATA Frame Policy** — Added `Client(h2_data_frame_policy=...)` with `H2DataFramePolicy.BROWSER_DEFAULT`, `.PEER_MAX_FRAME_SIZE`, `.fixed_payload(n)`, and `.socket_write_aligned(n)`, choosing how request bodies are split into DATA frames. The default keeps the browser preset's native framing, since frame sizes are themselves an observable fingerprint trait.
|
|
59
|
+
- **Strict TLS close_notify** — Added `Client(require_close_notify=True)` and a matching read-only property. When enabled, a response body truncated by a bare socket EOF — with no TLS `close_notify` alert — is reported as an error instead of being accepted as a complete body. One part applies regardless of the flag: the read loop now stops at `close_notify` instead of reading on, because anything arriving after it did not come from the peer — those bytes were previously appended to the response body.
|
|
60
|
+
- **Trust Anchor IDs Extension** — Added `ExtType.TRUST_ANCHOR_IDS` and a new `ExtensionSpec` source: `ExtensionSpec(ExtType.TRUST_ANCHOR_IDS, source="trust_anchor_ids", trust_anchor_ids=[...], shuffle=False)`, with `trust_anchor_ids` and `shuffle` readable back off the spec.
|
|
61
|
+
- **DNS-over-HTTPS Feature** — Upstream moved the DoH resolvers behind a `doh` Cargo feature. The published wheels enable it, so `dns="google_https"`, `"cloudflare_https"` and `"quad9_https"` keep working exactly as before; a reduced-feature source build now rejects those three names with an actionable error instead of a generic "unknown DNS config".
|
|
62
|
+
|
|
63
|
+
### 🐞 Fixes
|
|
64
|
+
- **A Rejected ECH No Longer Falls Through In The Clear** — when a server did not accept Encrypted ClientHello, the handshake previously carried on and handed back a connection whose real server name had never been protected, so the request ECH existed to hide went out in the open. The handshake now aborts before any traffic keys exist, and the session redials once with the `retry_configs` the server supplied — the same thing a browser does, so routine ECH key rotation stays transparent. Retrying *without* ECH is deliberately not a fallback. This affects clients that never set `ech_config=`, because ECH is enabled automatically from DNS HTTPS records; a rejection that cannot be retried now surfaces as `TlsError`. GREASE ECH is unaffected.
|
|
65
|
+
- **Status-Code Retries Are Gated On Replay Safety** — a non-idempotent request answered with a status in the retry set was replayed even though the answer proves the server received it, so a `POST` answered `502` was sent twice. Such a method is now retried only on a status that says it was *not* applied (`429`, `503`); `502`, `504` and the Cloudflare `520`–`530` range are returned to the caller instead. Safe methods are unaffected, and passing `idempotency=Idempotency.Idempotent` on the request restores the old behaviour per call.
|
|
66
|
+
- **Only Real Redirect Statuses Are Followed** — `300`, `304`, `305` and `306` are no longer treated as redirects. Previously a `304 Not Modified` or a `305 Use Proxy` could send the client chasing a `Location` header it should have ignored. `301`, `302`, `303`, `307` and `308` are unaffected.
|
|
67
|
+
- **Cookie Name Prefixes Enforced** — Cookies named with the `__Secure-` or `__Host-` prefix are now rejected unless they satisfy the prefix rules (`Secure`, and for `__Host-` also a `/` path with no `Domain`). A server can no longer set a lookalike cookie that client code would reasonably trust.
|
|
68
|
+
- **Secure Cookies Ignored Over Plain HTTP** — A `Set-Cookie` carrying `Secure` received over a plain-HTTP channel is now discarded rather than stored.
|
|
69
|
+
- **Sessions Are No Longer Capped At 64 Connections** — The implicit per-session ceiling made unrelated high-concurrency workloads fail with `session connection limit reached`. The default is now unbounded; set `max_connections_per_session` explicitly to impose a budget.
|
|
70
|
+
- **Multi-Layer Content-Encoding Is Decoded** — A response carrying stacked encodings (for example `Content-Encoding: gzip, br`) is now fully decoded instead of being returned partly compressed. The zlib-wrapped form of `deflate` is also accepted, matching what browsers tolerate.
|
|
71
|
+
- **WebSocket Handshakes And Close Codes Are Validated** — the upgrade response was checked only for a `101`, which on its own proves nothing; a wrong or missing `Sec-WebSocket-Accept` merely logged a warning. RFC 6455 §4.1 makes that, a missing or wrong `Upgrade`/`Connection`, and any unoffered extension or subprotocol grounds for failing the connection, and all are enforced now. Enforcing it also uncovered that `Sec-WebSocket-Accept` was being computed with a GUID that is not the one §1.3 specifies, so the token never matched a conforming server — harmless while the check only warned. On the sending side, `1005`, `1006` and `1015` may never appear on the wire (§7.4.1) and are now rejected locally, along with over-long close reasons, instead of producing an invalid frame.
|
|
72
|
+
- **HSTS Is Learned Only From Secure Transport** — `Strict-Transport-Security` is no longer recorded from a plain-HTTP response, and never for IP-literal hosts.
|
|
73
|
+
- **Multipart Bodies Match Chrome** — Boundary generation and parameter escaping now follow Chrome's format. A `Content-Type` containing CR or LF is rejected outright, since — unlike the quoted `Content-Disposition` parameters, which are escaped — it cannot be encoded safely and would be a live header-injection vector.
|
|
74
|
+
- **Redirect Method Rewriting Follows The Spec** — The request method is now rewritten only in the cases the specification calls for, so `307` and `308` correctly preserve the original method and body.
|
|
75
|
+
- **Graceful HTTP/2 GOAWAY Is Honoured** — a graceful shutdown carries `NO_ERROR` and therefore set no connection error, while the draining check keyed off "has an error". The driver kept opening streams on a connection the peer had already promised to refuse. Google and Cloudflare send these routinely for connection turnover, so this showed up as sporadic failures under sustained load; queued requests now fail over to a fresh connection.
|
|
76
|
+
- **Alternative Services No Longer Supply The TLS Identity** — an Alt-Svc entry or DNS HTTPS record says *where* to send packets, but the endpoint must still prove it may serve the origin. The advertised host was used as the SNI and certificate name, so the alternative authenticated as itself. Besides the trust problem this broke conforming deployments outright, since an HTTPS RR `TargetName` is typically a CDN-internal name serving the origin's certificate.
|
|
77
|
+
- **Certificates Compressed With zstd Decompress** — `compress_certificate` advertises zlib, brotli and zstd (the Firefox 147 profile offers all three), but only zlib and brotli could be decoded, so a server that picked zstd failed the handshake. All three are handled now (RFC 8879).
|
|
78
|
+
- **Coalesced TLS Post-Handshake Messages Are Reassembled** — post-handshake messages follow the same framing as the handshake: several may share one record, and one may span several. Each record was treated as exactly one message, so a `KeyUpdate` sharing a record with a `NewSessionTicket` was silently dropped — read keys were never rotated and every record after it failed to decrypt — and coalesced tickets past the first were lost.
|
|
79
|
+
- **Protocol Hardening Across TLS, HTTP/2, QUIC And Alt-Svc** — Picks up a broad upstream compliance pass: TLS 1.2 renegotiation is refused with a fatal alert, the record layer and session-ticket trust are bounded, servers can no longer select parameters the client never offered, HTTP/2 enforces response pseudo-header rules and closes connections on peer protocol errors, `record_size_limit` is honoured when sending (RFC 8449), draft HTTP/3 Alt-Svc advertisements are ignored, and SOCKS5 authentication replies are version-checked.
|
|
80
|
+
|
|
81
|
+
## lkrequest-py 0.2.0
|
|
82
|
+
|
|
83
|
+
### 🚀 Features
|
|
84
|
+
- **System DNS Positive Cache** — Added `system_dns_cache_ttl` and `system_dns_cache_max_entries` to `Client` and `BlockingClient`. The OS resolver already coalesces concurrent identical lookups process-wide; these cache successful, non-empty results for `ttl` seconds (default capacity 1024 entries). A `0` TTL keeps the coalescing while disabling the cache. It selects the system resolver, so it cannot be combined with `dns`; errors and empty results are never cached.
|
|
85
|
+
- **Shutdown Draining** — Added `drain_pending(timeout=None)`, its blocking counterpart `blocking_drain_pending(...)`, and `pending_requests()`. Waiting for in-flight work before tearing down an event loop means results are still delivered rather than dropped on arrival. Counts are process-wide and cover every async call, not only requests.
|
|
86
|
+
- **HTTP/2 Pending-Request Bound** — Added `max_pending_h2_requests` to `Client` and `BlockingClient`. HTTP/2 requests that exceed the peer's concurrent-stream limit now wait for a stream slot instead of failing, and this option caps how many may queue. Omit it (the default) for an unbounded queue; `0` is rejected with `ValueError`.
|
|
87
|
+
- **Synthetic Fingerprint Negotiability (`synthetic-fp`)** — Added `NegotiabilityFloor.UNIVERSAL`, `NegotiabilityFloor.PRESET_FAMILY`, `NegotiabilityFloor.custom(...)`, and `Randomize.negotiability(...)` so synthesized ClientHello profiles retain a configurable signature-algorithm compatibility floor.
|
|
88
|
+
- **Multi-hop Proxy Chains** — Added `ProxyConfig.parse_chain(...)` and `.through(...)`, with chain metadata via `hop_count()` and `identity()`. Client sessions, proxy pools, and session pools now accept either proxy URLs or `ProxyConfig` objects.
|
|
89
|
+
- **Mixed Proxy Transports** — HTTP/1.1 and HTTP/2 can traverse ordered combinations of HTTP CONNECT, SOCKS5, and SOCKS5H hops. QUIC/HTTP3 supports all-SOCKS5 chains when the optional `quic-h3` feature is enabled.
|
|
90
|
+
- **QUIC-TLS Profile Presets** — Added `TlsProfile.chrome_146_quic()`, `TlsProfile.chrome_150_quic()`, and `TlsProfile.chrome_151_quic()`, exposing the ClientHello Chrome sends inside QUIC (previously reachable only through the bundled `Client.chrome_1xx()` presets). Pass one to `Client(quic_fingerprint=...)` when building a client manually; HTTP/3 otherwise reuses the main TLS profile.
|
|
91
|
+
- **Chrome 151 Preset** — Added `Client.chrome_151()`, `BlockingClient.chrome_151()`, `TlsProfile.chrome_151()`, `H2Profile.chrome_151()`, `QuicProfile.chrome_151()`, and the `"chrome_151"` TLS/H2/QUIC profile names. Captured from Chrome 151.0.7922.72: TCP TLS and HTTP/2 keep Chrome 150's stable fields, while the QUIC-TLS capture omits the three ML-DSA signature algorithms; QUIC transport parameters and HTTP/3 SETTINGS stay aligned with Chrome 150.
|
|
92
|
+
- **Chrome 150 QUIC/H3 (`quic-h3`)** — `Client.chrome_150()` now uses dedicated Chrome 150 QUIC TLS and HTTP/3 profiles instead of reusing Chrome 146. The preset covers captured signature algorithms, QUIC transport parameters, QPACK limits, navigation `PRIORITY_UPDATE`, connection-ID lengths, and Initial datagram sizing. Added `QuicProfile.chrome_150()` and the `"chrome_150"` QUIC profile name.
|
|
93
|
+
|
|
94
|
+
### 🐞 Fixes
|
|
95
|
+
- **No More Crashes From Late-Completing Requests** — A request that finished after its event loop had closed used to print a `RuntimeError: Event loop is closed` traceback from whichever worker thread it landed on. Enough of them at once turned into `Fatal Python error: _enter_buffered_busy` and killed the process with SIGABRT — uncatchable, and it took the whole batch down. Results arriving after the loop closes are now dropped silently, since nothing is left to receive them.
|
|
96
|
+
- **Multi-Address Connect Fallback** — Connecting now tries every address a hostname resolves to, racing them Happy Eyeballs style (RFC 8305) with interleaved address families, rather than giving up when the first candidate is unreachable. Applies to direct TCP and to SOCKS5 proxies.
|
|
97
|
+
- **HTTP/2 Stream-Limit Backpressure** — Requests beyond the peer's advertised `MAX_CONCURRENT_STREAMS` now wait asynchronously for a stream slot instead of failing with a local capacity error.
|
|
98
|
+
- **Redirects Over Reused HTTP/1.1 Connections** — Following a redirect no longer fails when the pooled HTTP/1.1 connection was torn down in the meantime. The reuse attempt surfaces as hyper's `operation was canceled`, which is now recognised as a closed connection, so the request is retried on a fresh connection instead of erroring out. Affects any server that hangs up after answering a `3xx` — a common pattern after `POST`.
|
|
99
|
+
- **HTTP/2 `1xx` Interim Responses** — The HTTP/2 driver now consumes `1xx` informational responses — such as the `103 Early Hints` Cloudflare sends ahead of its `200` — and returns the final response that follows, instead of reporting the interim status as the final one. A plain `GET https://www.cloudflare.com/` previously came back as `103`.
|
|
100
|
+
- **Connection Limit Enforcement** — `max_connections` now bounds *physical* connections, counting those still being dialled and those checked out by an in-flight request rather than only what sits idle in the pool. Exceeding the limit raises instead of quietly opening another connection, and an idle HTTP/1.1 connection is evicted first so one origin's idle connections cannot starve another. `PoolStats.total` correspondingly reports physical connections in use rather than pooled entries.
|
|
101
|
+
- **Optional Client Certificate Requests** — A server that asks for a client certificate no longer breaks the handshake. TLS 1.2 and TLS 1.3 now answer a `CertificateRequest` with an empty client `Certificate` message — echoing the request context on TLS 1.3, as RFC 8446 §4.4.2 requires — so endpoints with optional mutual TLS complete the handshake instead of failing.
|
|
102
|
+
- **TLS Handshake Reassembly** — TLS 1.2 and TLS 1.3 handshake messages fragmented across multiple records are now buffered and reassembled correctly, preventing failures such as `truncated handshake message in record` without changing ClientHello or fingerprint output.
|
|
103
|
+
- **SOCKS5 UDP Relay Compatibility** — QUIC-over-SOCKS5 accepts relay replies from the advertised source IP even when the relay uses a different egress port, preventing affected HTTP/3 requests from hanging.
|
|
104
|
+
- **Package Version Reporting** — `lkrequest.__version__` is now read from installed distribution metadata instead of being maintained as a separate hard-coded value.
|
|
105
|
+
- **TLS 1.3 ECH + HelloRetryRequest** — Real ECH CH2 now reuses the ECH configuration, HPKE sender context, Inner random, GREASE values, extension permutation, and fake ECH bytes. HRR confirmation, selected groups, cookie-only retries, invalid extensions, dummy CCS, and local fatal alerts now follow the browser/RFC flow.
|
|
106
|
+
- **ECH + PSK + HRR Binders** — `pre_shared_key` remains exclusively in `ClientHelloInner`; CH1 and CH2 binders now use the correct truncated Inner transcripts, including `message_hash(ClientHelloInner1) + HRR` for CH2.
|
|
107
|
+
- **QUIC/H3 HRR State Transitions** — QUIC revokes 0-RTT stream and packet state after HRR and maps local TLS alerts to QUIC `CRYPTO_ERROR` codes.
|
|
108
|
+
|
|
109
|
+
## lkrequest-py 0.1.1
|
|
110
|
+
|
|
111
|
+
### 🐞 Fixes
|
|
112
|
+
- **Project URLs** — `Homepage`, `Repository`, and `Issues` in the package metadata pointed at a repository path that does not exist, so every link on the PyPI project page returned 404. They now point at `github.com/EZ-XLabs/lkrequest-py`.
|
|
113
|
+
- **README Links on PyPI** — the language switcher at the top of both READMEs used relative paths (`README.md` / `README-zh.md`), which PyPI cannot resolve when it renders the long description. Both now use absolute URLs, so switching between English and 简体中文 works from the project page.
|
|
114
|
+
|
|
115
|
+
### 📝 Documentation
|
|
116
|
+
- **Development Setup** — the setup section now walks through the full local workflow on Linux/macOS and Windows: creating and activating a virtualenv, installing maturin, `pip install -e ".[test,lint]"`, and running the suite via `just test`. An equivalent `uv sync --extra test --extra lint` path is documented alongside it.
|
|
117
|
+
|
|
118
|
+
## lkrequest-py 0.1.0
|
|
119
|
+
|
|
120
|
+
### 🚀 Features
|
|
121
|
+
- **TLS Fingerprint Control** — Byte-level ClientHello generation that matches real browsers (Chrome, Firefox, Safari)
|
|
122
|
+
- **HTTP/2 Fingerprint Control** — SETTINGS frame order, pseudo-header order, WINDOW_UPDATE, PRIORITY frames, and per-request priority weights
|
|
123
|
+
- **HTTP/3 + QUIC** — Optional `quic-h3` feature for full HTTP/3 over QUIC with fingerprint-aware Transport Parameters, Alt-Svc auto-discovery, and H2→H3 seamless upgrade
|
|
124
|
+
- **Built-in Browser Presets** — Chrome 131/144/145/146/147/148/149/150, Firefox 133/147, Safari 18/26 (TLS + H2, plus QUIC when `quic-h3` is enabled)
|
|
125
|
+
- **Session Management** — Cookie jars, connection pooling, HTTP/2 multiplexing, and optional QUIC 0-RTT session resumption
|
|
126
|
+
- **Connection Prewarming** — `session.preconnect()` pre-establishes DNS + TCP + TLS + H2 (or, with `quic-h3`, QUIC + H3) connections
|
|
127
|
+
- **Custom DNS Resolver** — Pluggable DNS with DoH support, auto ECH config and H3 hints via HTTPS/SVCB records
|
|
128
|
+
- **Alt-Svc Discovery** — Automatic HTTP/2 → HTTP/3 protocol upgrade via Alt-Svc headers with broken-QUIC fallback
|
|
129
|
+
- **SessionPool** — High-concurrency pool with proxy rotation (round-robin / random)
|
|
130
|
+
- **Proxy Support** — SOCKS5 and HTTP CONNECT with authentication
|
|
131
|
+
- **WebSocket** — HTTP/1.1 Upgrade (RFC 6455) and H2 Extended CONNECT (RFC 8441)
|
|
132
|
+
- **Middleware** — Interceptor chain for request/response modification
|
|
133
|
+
- **Retry Policies** — Exponential backoff, fixed interval, custom strategies; idempotency-aware (non-idempotent methods are not silently replayed after a possibly-already-sent failure unless marked `Idempotency::Idempotent`)
|
|
134
|
+
- **Redirect Control** — `RedirectPolicy::Follow(n)` or `RedirectPolicy::None` for manual handling
|
|
135
|
+
- **Auto-Decompression** — Brotli, gzip, deflate, zstd
|
|
136
|
+
- **Blocking API** — Synchronous wrapper for non-async contexts
|
|
137
|
+
- **Multipart/Form-Data** — File uploads with streaming support
|
|
138
|
+
- **TCP Fingerprint** — JA4T-style TCP SYN fingerprinting
|