All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.3.0 - 2026-08-20
- TLS errors and certificate overrides (#4):
- a failed handshake now returns
NetError::Tls(TlsError)instead of an opaque client error.TlsErrorhas aTlsErrorKind(Expired,NotYetValid,UnknownIssuer,HostnameMismatch,Revoked,InvalidCertificate,Handshake,Other), the host and the rustls message. Observers getNetEvent::TlsFailed. FetcherConfig::tls_overridestakes aTlsOverrideStore(in-memory default:InMemoryTlsOverrideStore) and enables browser-style "proceed anyway": the error then carries the certificate and its fingerprint,store.accept(host, fingerprint)lets the next connection through, andFetcherContext::tls_overridecan accept on the spot. Per (host, certificate); refused for HSTS hosts and for non-certificate failures.test-support:TestServer::tls_validitysets the certificate's validity window, to test expired and not-yet-valid certificates- native-only; on wasm32 the browser does TLS
- a failed handshake now returns
- CORS per WHATWG Fetch (#2), enforced per redirect hop whenever a request carries
FetchRequest::origin; without one CORS is entirely inert, like mixed content:RequestModeis now enforced:SameOriginrefuses cross-origin targets,NoCors(the default) only allows cross-origin loads in the shape markup can produce and marks the response opaque,Corsruns the CORS check on every response of the chain — redirect hops included- preflights: an
OPTIONSround-trip when the method or headers need server approval, re-run per hop after redirects; grants cached per (origin, URL, credentials) honoringAccess-Control-Max-AgeviaFetcherConfig::cors_preflight_cache(CorsPreflightCachetrait, in-memory default) FetchRequest::credentials(RequestCredentials, defaultInclude— the old behaviour) gates cookie-jar injection per hop and selects the credentialed CORS rules- response tainting is annotated, not enforced:
FetchResultMeta::tainting+readable_headers()compute the script-visible view; the embedder owns the boundary - failures surface as
BlockReason::Cors(CorsError), preflights asNetEvent::CorsPreflight; redirectLocations with embedded credentials are refused - inert on wasm32;
test-supportgainsRouteConfig::Corsand separateOPTIONShit counts
OriginandSec-Fetch-*fetch metadata headers (#47):Sec-Fetch-Dest,Sec-Fetch-Mode, andSec-Fetch-Siteare sent on every request, driven by the newFetchRequest::destinationandFetchRequest::modefields (RequestDestination/RequestMode, re-exported at the crate root);Sec-Fetch-User: ?1is sent on user-activated navigations (Initiator::User)Originis sent on non-GET/HEAD requests and on cross-origin CORS/WebSocket requests, computed from the existingFetchRequest::originfield- like
Referer, the values are recomputed at every redirect hop and only sent to potentially trustworthy targets;Sec-Fetch-Sitecan only degrade across a chain, andOriginbecomesnullafter a tainting cross-origin redirect same-sitecompares registrable domains using the public suffix list (pslcrate, native only) (#60); same host on another port reportssame-site- inert on wasm32, where the browser owns these headers
- Runnable examples:
document_fetch,fetcher_context,streaming, andtls_override
-
Per-origin concurrency limits now follow the negotiated protocol (#49): every origin starts at the HTTP/1.1 limit (
h1_per_origin, default 6) and is raised to the HTTP/2 limit (h2_per_origin, default 16) once an HTTP/2 or HTTP/3 response has been seen from it, redirect hops included. Previously everyhttpsorigin was assumed to speak HTTP/2, giving HTTP/1.1-only servers 16 connections instead of 6. -
Breaking:
FetchRequestgains thecredentialsfield,FetchResultMetagainstainting,BlockReasongainsCors(CorsError),NetEventgainsCorsPreflightandTlsFailed, andNetErrorgainsTls— struct-literal construction and exhaustivematches need updating. Requests built throughFetchRequest::builder()keep their previous behaviour (modeNoCorsrestrictions aside) -
Request coalescing now also keys on the credentials mode, so requests that would attach different cookies never share a response
-
The default
User-Agentis nowgosub-sonar/<crate version>instead of no header at all (#38). The value is available asDEFAULT_USER_AGENT; setFetcherConfig::user_agentto override it, or toNoneto send noUser-Agentheader.
- Streamed bodies lost data when the subscriber attached after bytes had already been read:
the first chunk beyond the peek buffer, and everything the server sent before the caller got
around to
subscribe_stream(), was pushed to nobody.SharedBody::from_readernow waits for the first subscriber before it starts reading (bounded by the idle/total timeouts and cancellation), and subscribing after the body ended with an error yields that error instead of an empty stream.
0.2.0 - 2026-08-01
-
wasm32 support: the crate now compiles for
wasm32-unknown-unknown, where the browser'sfetch()provides the transport. The async API (Fetcher,simple_get) is available there; native-only pieces — the blocking helpers (sync_get,sync_fetch),file://URLs, HSTS, proxy configuration, the DNS resolver, and streaming uploads — are compiled out, with the browser applying its own equivalents where they exist. CI builds the wasm32 target. -
Pluggable DNS resolution —
FetcherConfig::dns_resolvertakes aDnsResolverimplementation which becomes the only resolver the underlying client consults: every lookup, including each redirect hop, goes through it, and connections go to exactly the addresses it returns. Lookups happen per connection, not per request, so a rebound DNS name cannot redirect a pooled connection (DNS rebinding). ReturnErrto refuse a host — the shape of an SSRF policy that classifies resolved addresses rather than URLs.DnsResolver,DnsError, andResolvingare re-exported at the crate root. Native-only: on wasm32 the browser owns name resolution. -
Proxy configuration (#12) —
FetcherConfig::proxytakes aProxyConfig, so an embedder can point the fetcher at a proxy from its own settings instead of the process environment:ProxyConfig::System(the default) keeps the previous behaviour, readingHTTP_PROXY,HTTPS_PROXY,ALL_PROXY, andNO_PROXYProxyConfig::Disabledconnects directly and ignores those variablesProxyConfig::Rulesuses only the rules given. AProxyRulecarries aProxyScope(Http/Https/All), the proxy URL, optionalProxyAuth(Basicor a verbatimProxy-Authorizationvalue), and an optionalNO_PROXY-syntax bypass list- an unusable proxy URL or auth header is reported by
Fetcher::new - new
sockscargo feature to acceptsocks4/socks5/socks5hproxy URLs - native-only: on wasm32 the browser's
fetch()applies the user's own proxy settings
-
tests/e2e.rs: integration tests exercising the crate through its public API only, as a downstream consumer would — including an externally implementedFetcherContext. Gated on thetest-supportfeature; CI now enables it. -
NetEventis re-exported at the crate root; implementingNetObserverpreviously required thenet::eventspath. -
HTTP Strict Transport Security (RFC 6797, dynamic part): a
Strict-Transport-Securityheader received over HTTPS is recorded, and laterhttp://requests to that host are rewritten tohttps://before any connection is opened. Enabled by default viaFetcherConfig::hsts, which holds anInMemoryHstsStoreunless you supply your ownHstsStore; set it toNoneto disable HSTS (e.g. for private browsing). The crate owns the protocol — header parsing,includeSubDomainsmatching, expiry, and the URL rewrite — so a store only has to behave like a map. No preload list. Native-only: on wasm32 the browser'sfetch()applies its own HSTS. -
NetPolicy::with_hstsfor callers using the low-levelfetchAPI directly. -
Streaming uploads:
RequestBody::streamtakes a reader factory (opened once per send attempt, so 307/308 redirects can replay the body), andRequestBody::filestreams a file from disk without buffering it. Native targets only. -
Connection-pool tuning in
FetcherConfig:pool_max_idle_per_host(default 6),pool_idle_timeout(default 90s), andtcp_keepalive(default 60s). Previously reqwest's defaults applied: an unbounded idle pool and no keepalive. -
test-support: the mock server can now serve HTTPS —TestServer::tls(domain)withTestServerHandle::{cert_pem, socket_addr, tls_domain}—RouteConfig::ok_with_headersresponds 200 with arbitrary extra response headers, andRouteConfig::redirect_307issues a 307 that preserves the method and body. -
Mixed content blocking (#5) — insecure sub-resources requested by a secure document are blocked, or upgraded to
https, at every redirect hop:net::mixed_content—MixedContentPolicy(Allow/Upgrade/Block) and the secure-context predicatesFetcherConfig::mixed_content— fetcher-wide default (Block)FetchRequest::origin— the initiating document's origin; unset leaves the check inertFetchRequest::mixed_content— per-request override, to permit images while still blocking scripts
-
Referrer policy (#6) — a
Refererheader computed per the Referrer Policy spec, recomputed at every redirect hop and retargeted mid-chain by aReferrer-Policyresponse header:net::referrer— all eightReferrerPolicyvalues, defaulting tostrict-origin-when-cross-originFetchRequest::referrer— the initiating document's URL; unset sends no headerFetchRequest::referrer_policy— how much of it to reveal
-
NetError::Blocked/NetEvent::Blocked, with a typedBlockReason -
test_support:RouteConfig::RedirectAbsolute,EchoRefererHeader,RedirectWithReferrerPolicy, andRecordingObserver
RequestBody'sbytesfield is private; useRequestBody::as_bytes().len()now returnsOption<u64>(Nonefor a stream without a declared length).- Breaking: scheme and
is_url_allowedrejections now returnNetError::Blockedinstead ofNetError::Redirect/NetError::Other - Breaking:
NetErrorandNetEventgain aBlockedvariant, andFetchRequestandRequestInitgain public fields — exhaustivematches and struct-literal construction need updating - Request coalescing now also keys on the mixed content verdict and the referrer, so fewer requests share a response
FetchRequest::builder()now defaults toauto_decode: true, matching the simple API and the wasm32 build. Use.with_auto_decode(false)for raw bytes.- With decoding on,
max_bytescaps the decompressed size, and the earlyContent-Lengthrejection no longer applies (reqwest strips the header when it decodes).
- Breaking:
FetchHandleandFetchKeyDataare no longer part of the public API. The request-coalescing key is an internal detail of the fetcher, and everything the handle carried is available fromFetchResult/FetchResultMeta.
- The URL policy is now applied to redirect targets.
build_clientnever disabled reqwest's own redirect following, so reqwest resolved each 3xx internally and the manualget_with_redirectsloop only ever saw the final response.FetcherContext::is_url_allowedwas therefore consulted for the initial URL but not for any redirect target, contrary to its documentation — a redirect to an internal address bypassed an embedder's SSRF guard. TheSet-Cookie-on-3xx jar reporting and the cross-originAuthorization/Cookiestripping were inert for the same reason and are now live. Refereris now stripped on cross-origin redirects, alongsideAuthorizationandCookie, so a hand-set one cannot leak to a third-party host
0.1.0 - 2026-07-04
Initial release. gosub-sonar is the network stack of the Gosub browser engine, extracted into a standalone, browser-agnostic crate.
Fetcher— priority-scheduled fetcher with:- four priority lanes (
High,Normal,Low,Idle) dequeued via weighted round-robin, so lower priorities never starve - request coalescing: identical in-flight GET/HEAD requests share one HTTP request, with fan-out of the response to all subscribers
- global and per-origin concurrency limits (separate HTTP/1.1 and HTTP/2 caps)
- per-subscriber cancellation (
fetch_with_cancel); the underlying request is aborted once all subscribers cancel - buffered and streaming response bodies (
FetchResult::Buffered/Stream), with a peek buffer for content-type sniffing and an optionalmax_bytescap
- four priority lanes (
FetcherContexttrait for lifecycle integration: URL filtering (scheme allowlist / SSRF policy), cookie jar hooks (cookies_for,on_cookies_received, including on intermediate redirect hops), and observer selection per request;NullContextfor when none of this is neededFetchRequestbuilder: method, headers, body, priority, initiator, resource kind, streaming, auto-decode, and byte-limit settings- Request bodies (
RequestBody::bytes/json/form/text) with redirect method semantics per RFC 7231 §6.4 - Content decoding (gzip, brotli, deflate) behind a per-request
auto_decodeflag - Redirect handling with a hop limit, plus typed
NetErrorvariants (reqwest, redirect, I/O, cancelled, read, timeout) NetObserver/NetEvent— progress, redirect, header, and completion events for every request- Simple one-shot API: async
simple_get, blockingsync_get(bytes), and blockingsync_fetch(fullResponsewith status, headers, and cookies) test-supportcargo feature: in-process mock HTTP server (TestServer) with configurable per-route behaviours (delays, mid-body stalls, connection drops, redirect loops, chunked bodies, gzip) for downstream integration tests- Runnable examples:
simple_fetch,fetcher, andfetcher_harness - No unsafe code (
#![forbid(unsafe_code)]); full public-API documentation