-
0518da3: Rust: move the redirect option off
RequestInitand onto the builder3.7.0 added
RequestInit.follow_redirects. Adding a public field to a struct consumers construct is a breaking change in Rust semver, shipped under a minor — building the SmooAI monorepo against 3.7.0 fails witherror[E0063]: missing fieldin 129 exhaustive constructors across ~40 crates.The option is now
FetchBuilder::with_follow_redirects, matching the shape Go and .NET already use, andRequestInitis back to its 3.6.2 fields. Redirect policy is per-Client in reqwest anyway, so the builder was the right home from the start.client::fetchkeeps its exact signature; a newclient::fetch_with_redirect_policytakes the extra argument, so nothing that compiled against 3.6.2 needs changing.3.7.0 is yanked from crates.io. The other four languages were unaffected — Python added a defaulted dataclass field, Go and .NET added builder methods.
-
43ca80a: Make redirect handling configurable in all five languages
Redirects were followed unconditionally everywhere, and TypeScript went further:
merge({}, init, { redirect: 'follow' })put the literal last, so a caller passingredirect: 'manual'had it silently overwritten. Python hardcodedfollow_redirects=Trueinto the httpx kwargs; Rust, Go and .NET set nothing and inherited platform defaults that follow up to 10 hops.That is a security gap, not just an ergonomic one. A caller who resolves a hostname and checks it against an SSRF allowlist has that guard defeated by a 302 to an internal address, because the check was performed on the original host. And RFC 8461 forbids following redirects when fetching an MTA-STS policy.
- TypeScript —
redirectis honoured (defaults first, caller last) - Python —
FetchOptions.follow_redirects - Rust —
RequestInit.follow_redirects: Option<bool>(Noneinherits, so a client-level default survives a per-request..Default::default()) - Go —
ClientBuilder.WithFollowRedirects, applied to a caller-supplied*http.Clienttoo - .NET —
SmooFetchOptions.FollowRedirects/WithFollowRedirects
Honouring the option was not sufficient on its own: in TS, Rust, Go and .NET a 3xx is neither "ok" nor "redirected", so it was raised as an error and the option was undone a line later. Each now returns a deliberately-unfollowed 3xx as an ordinary response. Defaults are unchanged — everything still follows unless a caller says otherwise.
- TypeScript —
-
8781ce8: Two places where a green result meant nothing.
The Go test lane ran
go testwithout-count=1. Go's test cache does not invalidate on a fixture read from outside the package directory, and the connect-timeout suite loadsspec/connect-timeout-corpus.jsonfrom the repo root — so a deliberately corrupted corpus still returnedok (cached).The release workflow gated the PyPI, crates.io, Go-tag and NuGet publishes on
steps.changesets.outputs.published == 'true'— i.e. on the npm publish succeeding in that same run. A run dying after npm left the follow-up run with no changesets to consume,published == 'false', every remaining step skipped, and a green check for a release that published nothing. Those steps now gate on being a publish run and are individually idempotent, aconcurrencygroup stops the workflow racing itself, and a final step fails the run if the released version is not live on npm, PyPI, crates.io and the Go tag.
- 1b26e3f: The Rust
SlidingWindowRateLimiterno longer reportsremaining_ms: 0when it rejects a request.Duration::as_millistruncates, so any sub-millisecond remainder came back as exactly zero — an error telling the caller to wait no time at all while refusing to serve them, which makes a caller that honorsremaining_msspin on the window boundary. It now rounds up to the next whole millisecond, andacquire()'s compensating+ 1is gone.
- 193b1e8: The Go port gains a real response-validation entry point:
RequestOptions.Validate func(data any) []string. Returning messages fails the request with a*SchemaValidationErrorcarrying them, whichDefaultRetryOptionsalready treats as non-retryable. Until nowSchemaValidationErrorwas a type the package defined, documented as "returned when response body validation fails", and never constructed — and the README's language matrix promised callers would get one. Go has no Standard Schema equivalent, so this is the seam rather than a bundled validator. LeavingValidateunset is behavior-identical.
- 932f253: The Rust crate
smooai-fetchnow uses rustls instead of native-tls.reqwest'sdefault-tlspulled inopenssl-sys, which needs a system OpenSSL and its headers at build time — a cross-compile and container-image hazard for a library consumed across the platform.openssl-sysandnative-tlsare gone from the lockfile.http2andcharsetare re-enabled explicitly, because disabling reqwest's default features would otherwise have silently downgraded every consumer to HTTP/1.1.
- cd9f9ed: CI runs one job per language instead of a single serial
validatejob, so a failure in one port no longer hides the verdict of the other four. Two test-visibility gaps closed alongside it:vitest --passWithNoTestsis gone (an empty TypeScript suite went green), and the Rust lane now runs--all-features, which is what actually compilestests/trace_propagation_tests.rs— three real trace-propagation tests sat behind#![cfg(feature = "otel")]and reported "0 passed; ok" to a barecargo test.
-
a5434b0: Add an optional, default-off connect timeout to all five ports. It bounds only the connection-establishment phase, so a black-holed connect — a SYN to a dead pod IP still lingering in a ClusterIP's iptables — fails in ~that window and the configured retry can land on a live endpoint, instead of stalling until the whole-request timeout. Slow-but-alive handlers are unaffected, and leaving it unset preserves the previous behavior exactly.
- TypeScript:
connectTimeoutMs/FetchBuilder.withConnectTimeout(ms). Node only, via an undiciAgentdispatcher;undiciis an optional peer dependency, imported lazily and only when a connect timeout is requested. Ignored in browser/worker builds, which expose no such knob. - Python:
TimeoutOptions(connect_timeout_ms=...), mapped tohttpx.Timeout(connect=...). - Rust:
FetchOptions::connect_timeout_ms/FetchBuilder::with_connect_timeout, mapped toreqwest'sconnect_timeout. - Go:
ClientBuilder.WithConnectTimeout(d), applied to a cloned default transport's dialer. A caller-supplied*http.Clientis left untouched. - .NET:
SmooFetchOptions.ConnectTimeout/SmooFetchBuilder.WithConnectTimeout(ts), mapped toSocketsHttpHandler.ConnectTimeout. Not applied toIHttpClientFactory-owned handlers, which own their own handler.
All five regression tests read their knobs from
spec/connect-timeout-corpus.jsonso the timing thresholds cannot drift apart per language. - TypeScript:
-
5c8c71e: Drop two runtime dependencies from the published package.
@faker-js/faker— a test-data generator — was a runtime dependency, imported at module load, solely to build cosmetic names for the internal mollitia modules (smooai-fetch-retry-blue-cat). Those names only need to be unique within the process, so they come from a counter now.@standard-schema/utilswas declared but never imported by anything insrc/; it reaches this package transitively through@smooai/utils, which declares it itself.The remaining runtime dependencies (
mollitia,lodash.merge,@smooai/logger,@smooai/utils,@standard-schema/spec) are each load-bearing and stay.
-
d854c7c: Fix the release pipeline so shipped artifacts carry the version they claim, and give the Go module a resolvable path.
version:syncran afterchangeset publish, mutating manifests in the CI workspace that were never committed — so every git tag shipped stale version constants (go/fetch/v3.3.10containedconst Version = "2.1.2") andcargo publish --allow-dirtyexisted only to tolerate the dirt. The sync now runs insidechangeset version, so the bumped manifests land in the release commit;--allow-dirtyis gone; andnode scripts/sync-versions.mjs --checkruns in CI as a guard that fails loudly on any skew, including a pattern that stopped matching.The Go module path gains the
/v3major suffix Go requires above v1. Importgithub.com/SmooAI/fetch/go/fetch/v3(package identifier is stillfetch); tags throughgo/fetch/v3.4.0predate the suffix and do not resolve. The suffix is derived frompackage.json's major and is covered by the same guard, so a future major bump cannot leave it behind.
- 3f2cbd4: SMOODEV-2716: Redact credentials from everything the client logs.
@smooai/loggerperforms no redaction of its own, so theAuthorizationheader, the raw query string, the full URL in the log message and the request body were all reaching CloudWatch in plaintext on every request. A new internalredactmodule scrubs headers, query strings, URLs (including userinfo passwords) and bodies (url-encoded, JSON string and object forms) before they are handed to the logger; the request sent on the wire is unchanged. The Rust client gets the same treatment for the URL on itsSending HTTP requesttracing event. Both suites load the shared cases inspec/redaction-corpus.json. Python, Go and .NET log nothing about a request and so are unaffected.
-
2c20134: Rust: optional
otelfeature that injects W3C trace context (traceparent) into every outbound request, so a call made through this client continues the caller's trace instead of starting a new root. Off by default — the crate does not link OpenTelemetry unless you ask for it. Guards an invalid span context and never overwrites atraceparentthe caller set explicitly.TypeScript: the same injection at the same place — the single-request site, so every retry carries a current traceparent — behind an optional
@opentelemetry/apipeer dependency. Without it installed (or without a registered SDK) it is a no-op, not a crash, and no all-zerotraceparentis ever emitted.Python: the same injection at the same place — the single-request site, so retries carry a current traceparent — behind an optional
smooai-fetch[otel]extra. Withoutopentelemetry-apiinstalled it is a no-op, not an import error.Go: the same injection at the same place —
executeHTTPRequest, the single-request site inside the retry/timeout/breaker wrappers, so every attempt carries a current traceparent. Uses the OTel global propagator, which defaults to a no-op, so a service that never configured one sends nothing extra. Never overwrites a caller-settraceparent, and emits nothing at all when there is no valid span context..NET: no change needed —
HttpClient'sDiagnosticsHandleralready injectstraceparentfrom the currentActivity, ahead of redirects and connection pooling, and this client keeps that handler chain intact. Tests were added to pin that behaviour so a future custom primary handler cannot silently drop it.
- d174206: Migrate build tooling from tsup to tsdown — faster, oxc-based, drop-in replacement. The
esbuild-plugin-aliasshim used to swap@smooai/loggerNode entries for browser variants is replaced with@rollup/plugin-alias(rolldown-compatible). Output extensions shift from.js/.mjs/.d.tsto.cjs/.mjs/.d.cts/.d.mts(tsdown defaults); theexportsmap is updated to match. No public API change.
- 3607d13: SMOODEV-968: .NET — add sliding-window rate limiter to
SmooFetchBuilder.WithRateLimit(maxRequests, window, onRejected?). Built onSystem.Threading.RateLimiting.SlidingWindowRateLimiterso state is shared across every call on the constructedSmooFetch, matching the Rust / Go ports. Requests acquire a permit before dispatch and the optionalOnRejectedcallback fires for every would-be rejection for observability. Closes the parity gap left open by SMOODEV-946. - 23e86e9: SMOODEV-969: Python — share the sliding-window rate-limiter state across
fetch()calls made through a singleFetchBuilder. Previously_client.fetch()reconstructed the limiter per call, defeating the cross-call rate limit. The builder now lazily constructs oneSlidingWindowRateLimiter, hands the same instance to everyfetch()it dispatches, and rebuilds it when the caller changes options viawith_rate_limit. A newSlidingWindowRateLimiter.acquire_wait()method blocks until a slot is free (mirroring the Rust port'sacquireloop) so successive builder-mediated calls naturally queue instead of raisingRateLimitError. The low-levelfetch()entrypoint retains its raise-on-fullacquire()semantics for back-compat withrate_limit_retryplumbing.
- 62e0c22: SMOODEV-946: .NET port — close parity sweep. Adds
SmooFetchBuilderfluent API, Polly-based circuit breaker, lifecycle hooks (PreRequest/PostRequestOk/PostRequestErr),OnRejectionretry callback withOnRejectionDecision(Retry/RetryWithDelay/Abort/Skip/Default), andFastFirstonRetryPolicy. ExistingSmooFetchOptions+SmooFetch.Createfactory remain for backwards compatibility. Rate limiter is parked as a follow-up. - 148364b: SMOODEV-948: Async auth-token provider across TS, Python, Rust, Go. Adds a first-class hook that's invoked before every request to mint / refresh an auth token (sync or async), with the resulting
Authorizationheader injected using a configurable scheme (defaultBearer). Mirrors the existing .NETAuthTokenProviderdelegate. - ab2588b: SMOODEV-950: Circuit breaker — rate-based detection +
on_state_changecallback in Rust/Python/Go. Addsfailure_rate_threshold+sliding_window_sizefor rate-based tripping (Python, Rust) and anon_state_changecallback that fires on every state transition (Python, Rust, Go-builder). Mirrors the TSfailureRateThreshold+onStateChangesurface.
- 5fa920a: SMOODEV-949: Rate-limit-specific retry config in Rust + Python. Adds
RateLimitRetryOptions(an alias forRetryOptions, mirroring the Go port) plusFetchContainerOptions.rate_limit_retryand awith_rate_limit_retry(...)builder method. When configured alongside a rate limiter, rate-limit rejections are retried inside a dedicated inner loop rather than consuming the main retry budget.
- 620e2db: SMOODEV-947: Python port — close SMOODEV-627 retry parity. Add
on_rejectioncallback (RETRY/RETRY_WITH_DELAY/ABORT/SKIP/DEFAULT),fast_first(skip the initial retry delay), andmax_interval_ms(cap on per-retry delay) toRetryOptions. Brings the Python port in line with Rust + Go.
- e464834: SMOODEV-928: Bump
@smooai/loggerto^4.1.4and@smooai/utilsto^1.3.3. Picks up the ESM__filenameTDZ fix from logger 4.1.4 across the runtime dep graph (utils itself was on logger 3.x prior to 1.3.3). Also drops the deprecatedbaseUrl: "./"from tsconfig (TS 5.9+/6.x emit TS5101 withignoreDeprecations: "5.0"); fetch has nopathsentries so this is a no-op for type resolution.
- 9c9375d: SMOODEV-667: Fix release pipeline so PyPI + crates.io + NuGet actually publish.
pnpm buildproduces a Python wheel at the pre-sync version (the Cargo/pyproject bumps happen later, insideci:publish), so the publish step was trying to re-upload the stale wheel and getting rejected. Cleandist/beforeuv run poe publishso only the freshly-built version ships. Drop--lockedfrom the cargo publish step because sync-versions only updatesCargo.toml(notCargo.lock), which would trip--lockedas soon as crates.io is reached. Net effect:SmooAI.FetchNuGet package publishes for the first time; PyPI advances from the stalled 3.0.0.
- affe721: SMOODEV-666: Multi-target the SmooAI.Fetch NuGet package to
net8.0;net9.0;net10.0so consumers on every current .NET LTS + STS release get a nativelib/folder match. Polly v8, Microsoft.Extensions.Http, and Microsoft.Extensions.Http.Polly all resolve cleanly on all three TFMs — no per-TFM conditionals needed.
- 9cf41be: SMOODEV-664: Rewrite the .NET (NuGet) README to value-frame the package — lead with "HTTP that gets out of your way": typed JSON, automatic retries on transient failures, auth token injection, one error type per non-2xx. Drop the "Polly-backed" implementation lead. Republishes SmooAI.Fetch with the new README.
- 203479e: SMOODEV-662: Sync SmooAI.Fetch NuGet version to package.json + polish NuGet README
- 2662911: Add SmooAI.Fetch NuGet package — .NET 8+ port of @smooai/fetch with Polly-based retry (exponential backoff + jitter + Retry-After support), per-request timeout, HttpClientFactory integration, typed JSON helpers, async auth token provider, and typed HttpResponseError carrying status/body/headers.
- 0f57151: SMOODEV-627: Close TS→Rust/Go drift on retry options and builder surface. Rust + Go
RetryOptionsnow match TS:on_rejection/OnRejectioncallback (decisions: Retry with custom delay, Abort, Skip, Default), plusfast_first/FastFirstfor zero-delay first retry. Go also getsWithRateLimitRetry(opts)(configurable per-client rate-limit retry) andWithContainerOptions(FetchContainerOptions)batch setter mirroring TS's container-options ergonomics. Also gitignore.smooai-logs/so the pre-commit hook stops committing ephemeral test logs.
-
5d12e43: Add top-level
browserexport condition@smooai/fetchalready shipped a browser-safe build under./browser, but the top-level.entry had nobrowsercondition in the exports map. Browser bundlers (Vite, webpack withtarget: 'web', esbuild withplatform: 'browser') therefore resolvedimport fetch from '@smooai/fetch'to the Node entry, pulling@smooai/logger+rotating-file-stream+ other Node-only dependencies into the browser bundle.Adding the
browsercondition on.means consumers can now do:import fetch from '@smooai/fetch';
…and the bundler automatically picks the browser-safe dist when building for a browser target. No aliasing, no explicit
/browsersubpath import required.Consumers that were aliasing
@smooai/fetch→@smooai/fetch/browser/indexas a workaround (e.g.@smooai/config's tsup build) can drop that alias on upgrade.
- 001f556: Add explicit
./browsersubpath export soimport fetch from '@smooai/fetch/browser'resolves without the trailing/index. The existing./browser/*wildcard doesn't match the bare./browserspecifier per the Node.js exports spec — the*requires at least one character — so consumers previously had to write@smooai/fetch/browser/index, which contradicts the documented API. Adds a dedicated entry pointing atdist/browser/index.{mjs,js,d.ts}. The wildcard form continues to work for any future browser-side subpaths.
- ab17b63: Add Python, Rust, and Go language-specific READMEs with idiomatic usage examples, cross-language install table, and API reference.
- 8c0d28b: Implement fetch library in Python, Rust, and Go
- Python: httpx-based async client with custom circuit breaker, sliding window rate limiter, retry with Retry-After, pydantic schema validation, builder pattern (105 tests)
- Rust: reqwest-based async client with custom circuit breaker, sliding window rate limiter, retry with exponential backoff + jitter, thiserror errors, builder pattern (94 tests)
- Go: net/http client with sony/gobreaker circuit breaker, sliding window rate limiter, retry with Retry-After, builder pattern (76 tests)
- b9768f8: Update @smooai/logger and other smoo dependencies.
- a369ec7: Update SmooAI Packages link in README to point to smoo.ai/open-source for consistency across all SmooAI packages.
- 0f1a840: Update @smooai/logger and other smoo dependencies.
- 5893679: Update zod 3 to zod 4.
- 5893679: Update readme.
- 260482b: Update readme.
- d8ed851: Changed how we exported browser for better build safety.
- efd83d6: Update smoo dependencies.
- 7de1ffa: Update smoo dependencies.
- 53a3cc7: Added Browser export.
- 8e9855f: Fix package exports.
- 361a81a: Update readme.
- d4aecdc: Fix issue with JSON error message.
- 88f6e41: Fix issue with pre-using response body and update prettier plugins.
- 937a5cd: Changed FetchBuilder to take the schema in the constructor to fix type inference.
- 937a5cd: Updated all vite dependencies.
- 081e6ff: Fix package description.
- 7cbaa0b: Add lifecycle hooks to fetch implementation and update README
- Introduced lifecycle hooks: pre-request, post-response success, and post-response error, allowing for enhanced request and response handling.
- Updated README with detailed descriptions of lifecycle hooks and examples demonstrating their usage.
- Refactored fetch implementation to integrate hooks, improving flexibility and error handling capabilities.
-
7cbaa0b: Enhance README and fetch implementation with new options
- Added detailed section on opinionated defaults for the fetch function, including retry configuration, timeout settings, and rate limit retry options.
- Updated examples to demonstrate usage of new options in fetch requests.
- Introduced
RequestInitWithOptionstype to support additional options in fetch requests, within the same fetch argument footprint. - Improved error handling and response type inference in the fetch implementation.
This update aims to provide better guidance for users and enhance the flexibility of the fetch functionality.
- 07df8fe: Enhance fetch functionality with schema validation
- Enhanced fetch implementation with a FetchBuilder class for better configuration options, including schema validation, retry, and rate limiting.
- Improved error handling and logging capabilities in the fetch module.
- Updated README to reflect new features and usage examples.
- 3503fdb: Fix index export via @smooai/utils update.
- 4277a0f: Fix package file selection."
- 4d45f19: Fix npm publishing.
- 300d106: Fixed package.json for publishing.
- 8ceaebc: Updating @smooai/fetch to be its own package.
- 44fd23b: Fix publish for Github releases.
- 52c9eb1: Initial check-in.