What it is · Feature tour · Install · Quickstart · Language status · Examples · Platform
Stop writing the same retry logic over and over.
@smooai/fetchis a drop-infetchthat survives the reality of network failures — exponential backoff with jitter, timeouts,Retry-After-aware rate-limit handling, circuit breaking, lifecycle hooks, and typed responses — with native ports in five languages: TypeScript, Python, Rust, Go, and .NET. Same semantics everywhere; each port built idiomatically for its ecosystem.
Traditional fetch gives you the request, but leaves you to handle the reality of flaky APIs, slow endpoints, and rate limits. @smooai/fetch handles them by default.
One resilient HTTP client, ported natively to five languages. Every port carries the same core behaviors — verified against the source of each port, not aspirational:
- 🔄 Smart retries — exponential backoff with jitter to prevent thundering herds; retries only on network errors and retryable statuses
- ⏱️ Automatic timeouts — never hang indefinitely on slow endpoints (10s default, configurable per request)
- 🚦 Rate-limit respect — reads
Retry-Afterheaders and waits exactly what the server asked, plus a client-side sliding-window rate limiter - 🔌 Circuit breaking — stop hammering services that are clearly down
- 🔗 Lifecycle hooks — pre-request / post-response hooks for auth, logging, and metrics
- 🔑 Async auth token provider — register a token callback once; every request picks up a fresh token
- 📡 W3C trace-context propagation —
traceparentheaders injected automatically; OpenTelemetry is an optional integration in every port, never a hard dependency - 🎯 Typed responses — response typing and validation in every language, with mechanics that differ per ecosystem (see the honest matrix)
Each capability in a few lines of real, current API — snippets are verified against src/ and the language ports, not pseudocode.
| Capability | What you get | |
|---|---|---|
| 🔄 | Smart retries | Backoff + jitter, only on errors worth retrying |
| 🚦 | Rate-limit respect | Retry-After honored to the second, in all five ports |
| 🔌 | Circuit breaking | Fail fast when a dependency is down |
| 🎯 | Typed responses | Schema-validated data, typed end to end |
| 🔗 | Hooks + auth | One place for tokens, logging, and response policy |
| 📡 | Trace propagation | traceparent on every request, optional OpenTelemetry |
importfetchfrom'@smooai/fetch';// This won't crash if the API is temporarily downconstresponse=awaitfetch('https://flaky-api.com/data');// Behind the scenes:// Attempt 1: 500 error — waits ~500ms (jittered)// Attempt 2: 503 error — waits ~1000ms// Attempt 3: 200 success ✅Defaults (TypeScript): 2 automatic retries, exponential backoff starting at 500ms with factor 2, jitter to prevent thundering herds, and retries only on network errors or retryable HTTP statuses.
constresponse=awaitfetch('https://api.github.com/user/repos');// If GitHub says "slow down":// - Sees 429 + Retry-After: 60// - Automatically waits 60 seconds// - Retries and succeedsAll five ports parse Retry-After and wait what the server asked instead of the default backoff. A client-side sliding-window rate limiter (withRateLimit(100, 60000)) keeps you from hitting the ceiling in the first place.
import{FetchBuilder}from'@smooai/fetch';constcriticalAPI=newFetchBuilder().withCircuitBreaker({failureRateThreshold: 50,// open when ≥50% of calls fail…slidingWindowSize: 10,// …across the last 10 callsopenStateDelayMs: 30000,// stay open 30s, then trial a half-open call}).build();try{awaitcriticalAPI('https://payment-processor.com/charge');}catch(error){// Circuit is open — service is down. Show fallback UI immediately.}import{z}from'zod';constUserSchema=z.object({id: z.string(),email: z.string().email(),});constresponse=awaitfetch('https://api.example.com/user',{options: {schema: UserSchema},});// response.data is fully typed as { id: string; email: string }// No more runtime surprises in productionIn TypeScript, schema accepts any Standard Schema validator — Zod, Valibot, ArkType. The other ports type responses with their ecosystem's native tools; the language matrix says exactly which.
constapi=newFetchBuilder().withAuthTokenProvider(async()=>awaittokenStore.getFreshToken(),'Bearer').withHooks({postResponseError: (url,init,error)=>{if(error.response?.status===401){refreshToken();// Token expired — refresh and retry}returnerror;},}).build();Every port has both seams: an async auth-token provider (fresh token per request, no client rebuild) and pre-request / post-response hooks.
Every port injects a W3C traceparent header when a trace is active, so your HTTP calls join the distributed trace automatically. OpenTelemetry is an optional peer/feature in each language — the client works identically without it installed.
// With @opentelemetry/api installed and a span active:awaitapi('https://api.example.com/users/123');// → headers: { traceparent: '00-<trace-id>-<span-id>-01' }// Without it: same request, no traceparent, zero errors.%%{init: {'theme':'base','themeVariables':{
'background':'#020618','primaryColor':'#0b1426','primaryTextColor':'#e6edf6','primaryBorderColor':'#2b3a52',
'lineColor':'#7c8aa0','secondaryColor':'#0b1426','tertiaryColor':'#0b1426','fontFamily':'ui-sans-serif, system-ui, sans-serif',
'clusterBkg':'#0b1426','clusterBorder':'#22304a'}}}%%
flowchart LR
REQ["request"] --> PRE["pre-request hooks<br/>auth token · traceparent"]
PRE --> RL["rate limiter<br/>sliding window"]
RL --> CB["circuit breaker"]
CB --> RETRY
subgraph RETRY["retry loop — backoff + jitter, Retry-After aware"]
T["timeout"] --> HTTP["HTTP call"]
end
RETRY --> POST["post-response hooks"]
POST --> VAL["typed response<br/>schema / serde / generics"]
classDef warm fill:#f49f0a,stroke:#ff6b6c,color:#1a0f00;
classDef teal fill:#00a6a6,stroke:#00c2c2,color:#011;
class RETRY warm
class PRE,VAL teal
| Language | Package | Install |
|---|---|---|
| TypeScript | @smooai/fetch | pnpm add @smooai/fetch |
| Python | smooai-fetch | pip install smooai-fetch |
| Rust | smooai-fetch | cargo add smooai-fetch |
| Go | github.com/SmooAI/fetch/go/fetch/v3 | go get github.com/SmooAI/fetch/go/fetch/v3 |
| .NET | SmooAI.Fetch | dotnet add package SmooAI.Fetch |
Go note: the module path carries the
/v3major suffix Go requires above v1, so thego/fetch/v3.xtags resolve. The import path isgithub.com/SmooAI/fetch/go/fetch/v3; the package identifier is stillfetch. Tags minted before this change (throughgo/fetch/v3.4.0) point at commits whosego.modlacked the suffix and will not resolve — usev3.4.1or later.
Language-specific source lives in src/ (TypeScript), python/, rust/, go/, and dotnet/.
It's just fetch, but resilient — retries, timeout, and Retry-After handling are on by default in every port.
TypeScript (full docs)
importfetchfrom'@smooai/fetch';constresponse=awaitfetch('https://api.example.com/users/123');constuser=awaitresponse.json();Python (python/)
fromsmooai_fetchimportFetchBuilderbuilder=FetchBuilder().with_timeout(5000).with_retry()
response=awaitbuilder.fetch("https://api.example.com/users/123")Rust (rust/fetch/)
use smooai_fetch::fetch;use smooai_fetch::types::RequestInit;let response = fetch::<serde_json::Value>("https://api.example.com/users/123",RequestInit::default()).await?;Go (go/fetch/)
client:=fetch.NewClientBuilder().
WithTimeout(10*time.Second).
WithRetry(&fetch.DefaultRetryOptions).
Build()
resp, err:=fetch.Get[User](ctx, client, "https://api.example.com/users/1", nil).NET (dotnet/SmooAI.Fetch/)
varfetch=SmooFetch.Create(options =>{options.BaseUrl="https://api.example.com";options.RetryPolicy=RetryPolicy.ExponentialBackoff(maxRetries:3);});varuser=awaitfetch.GetAsync<User>("/users/me");// Node.jsimportfetchfrom'@smooai/fetch';// Browser — same API, different entry pointimportfetchfrom'@smooai/fetch/browser';constresponse=awaitfetch('/api/checkout',{method: 'POST',body: {items: cart},});Every port carries the shared core: retries with backoff + jitter, Retry-After handling, timeouts, a sliding-window rate limiter, a circuit breaker, lifecycle hooks, an async auth-token provider, and W3C traceparent propagation. The mechanics differ per ecosystem — same semantics, not byte-identical behavior:
| Language | Response typing / validation | Resilience engine | HTTP stack |
|---|---|---|---|
| TypeScript | Any Standard Schema validator (Zod, …) | mollitia | native fetch |
| Python | Pydantic models via with_schema(...) | implemented in-package | httpx |
| Rust | serde — fetch::<T> deserializes into your type | implemented in-crate | reqwest |
| Go | Generics — fetch.Get[User](...) decodes into your struct, plus an optional RequestOptions.Validate hook returning SchemaValidationError | implemented in-package | net/http |
| .NET | System.Text.Json — GetAsync<T> / PostAsync<TReq, TRes> (no pluggable validator) | Polly + System.Threading.RateLimiting | HttpClient / IHttpClientFactory |
Where a port leans on a battle-tested ecosystem library (mollitia, Polly), it says so above; the others implement retry/breaker/rate-limit logic natively, with each port's own test suite covering the shared behaviors.
| Language | What it logs about a request | Redaction |
|---|---|---|
| TypeScript | method, host, path, query string, headers, request body, and the URL in the message | full — headers, query, URL and body |
| Rust | method and URL, on one tracing::debug! event | URL only (userinfo password + query params) |
| Python | nothing | n/a — no logging sink |
| Go | nothing | n/a — no logging sink |
| .NET | nothing (an ILogger<SmooFetch> is held for DI but never called) | n/a — no logging sink |
This is not a parity gap. Redaction exists in exactly the two ports that have something to redact. Adding a scrubber to Python, Go or .NET would be code no call site reaches — which reads as a guarantee while guaranteeing nothing. The shared cases in spec/redaction-corpus.json are loaded by the TypeScript and Rust suites, and that file states the rule for anyone extending it: if a logging sink is ever added to another port, wire it to this corpus in the same PR.
Out of the box, @smooai/fetch is configured for the real world:
Retry strategy — 2 automatic retries, exponential backoff (500ms → 1s → 2s), jitter to prevent thundering herds, and retries only on network errors or retryable responses.
Timeout protection — 10-second default timeout, configurable per request, so requests never hang indefinitely.
Connect timeout (opt-in) — connectTimeoutMs / withConnectTimeout bounds only the connection-establishment phase, in all five ports. A black-holed connect then fails in ~that window and retry lands on a live endpoint, instead of burning the whole-request timeout on a dead one; slow-but-alive handlers are unaffected. Off by default. In TypeScript it needs the optional peer dependency undici and applies to Node only.
Rate-limit handling — respects Retry-After headers and backs off automatically on 429 responses.
constprimaryAPI=newFetchBuilder().withCircuitBreaker({failureRateThreshold: 50}).build();constfallbackAPI=newFetchBuilder().withTimeout(2000).build();asyncfunctiongetWeather(city: string){try{returnawaitprimaryAPI(`https://api1.weather.com/${city}`);}catch(error){console.warn('Primary weather API failed, using fallback');returnawaitfallbackAPI(`https://api2.weather.com/${city}`);}}@smooai/fetch works with @smooai/logger for complete observability across distributed systems.
importfetch,{FetchBuilder}from'@smooai/fetch';import{AwsServerLogger}from'@smooai/logger/AwsServerLogger';constlogger=newAwsServerLogger({name: 'APIClient'});constapi=newFetchBuilder().withLogger(logger)// That's it.build();// In Service Alogger.info('Starting user flow');// Correlation ID: abc-123constuser=awaitapi('/users/123');// Correlation ID sent as header// In Service B, the correlation ID is automatically extracted and logs are linked.Everything this client logs about a request — headers, query string, URL and body — is scrubbed of credential-bearing keys first, so an OAuth token exchange or a Bearer header does not land in CloudWatch in plaintext. Redaction is always on and applies only to the logged copy; the request on the wire is untouched.
A key is redacted when its normalized form (lowercased, -/_/. stripped) contains secret, password, passwd, token, apikey, authorization, credential, privatekey, assertion, cookie, session or signature, or equals auth, code, pwd or sig. The cases are pinned in spec/redaction-corpus.json, which both the TypeScript and Rust test suites load. client_id is deliberately not redacted — it is public in OAuth and load-bearing when debugging.
The Rust client redacts the URL it logs (userinfo password + query params); the Python, Go and .NET clients log nothing about a request, so they have nothing to redact.
When something goes wrong, you have the complete story — initial request, each retry attempt, circuit-breaker state changes, and the final error with a full stack trace:
try{constresponse=awaitapi('/flaky-endpoint');}catch(error){logger.error('Request failed after retries',error);}// In your logs:// {// "correlationId": "abc-123",// "message": "Request failed after retries",// "error": { "attempts": 3, "lastError": "TimeoutError", "circuitState": "open" },// "callerContext": { "stack": ["/src/services/UserService.ts:42:16"] }// }- Basic usage
- FetchBuilder pattern
- Retry
- Timeout
- Rate limit
- Schema validation
- Lifecycle hooks
- Predefined authentication
- Error handling
importfetchfrom'@smooai/fetch';// Simple GET requestconstresponse=awaitfetch('https://api.example.com/data');// POST request with JSON body and optionsconstresponse=awaitfetch('https://api.example.com/data',{method: 'POST',headers: {'Content-Type': 'application/json',},body: {key: 'value',},options: {timeout: {timeoutMs: 5000,},retry: {attempts: 3,},},});The FetchBuilder provides a fluent interface for configuring fetch instances:
import{FetchBuilder,RetryMode}from'@smooai/fetch';import{z}from'zod';constUserSchema=z.object({id: z.string(),name: z.string(),email: z.string().email(),});constfetch=newFetchBuilder(UserSchema).withTimeout(5000)// 5 second timeout.withRetry({attempts: 3,initialIntervalMs: 1000,mode: RetryMode.JITTER,}).withRateLimit(100,60000)// 100 requests per minute.build();constresponse=awaitfetch('https://api.example.com/users/123');// response.data is typed as { id: string; name: string; email: string }import{FetchBuilder,RetryMode}from'@smooai/fetch';// Using the default fetchconstresponse=awaitfetch('https://api.example.com/data',{options: {retry: {attempts: 3,initialIntervalMs: 1000,mode: RetryMode.JITTER,factor: 2,jitterAdjustment: 0.5,onRejection: (error)=>{if(errorinstanceofHTTPResponseError){returnerror.response.status>=500;}returnfalse;},},},});// Or using FetchBuilderconstfetch=newFetchBuilder().withRetry({attempts: 3,initialIntervalMs: 1000,mode: RetryMode.JITTER,factor: 2,jitterAdjustment: 0.5,onRejection: (error)=>{if(errorinstanceofHTTPResponseError){returnerror.response.status>=500;}returnfalse;},}).build();import{FetchBuilder}from'@smooai/fetch';// Using the default fetchconstresponse=awaitfetch('https://api.example.com/slow-endpoint',{options: {timeout: {timeoutMs: 5000,},},});// Or using FetchBuilderconstfetch=newFetchBuilder().withTimeout(5000)// 5 second timeout.build();try{constresponse=awaitfetch('https://api.example.com/slow-endpoint');}catch(error){if(errorinstanceofTimeoutError){console.error('Request timed out');}}import{FetchBuilder}from'@smooai/fetch';// Using the default fetchconstresponse=awaitfetch('https://api.example.com/data',{options: {retry: {attempts: 1,initialIntervalMs: 1000,onRejection: (error)=>{if(errorinstanceofRatelimitError){returnerror.remainingTimeInRatelimit;}returnfalse;},},},});// Or using FetchBuilderconstfetch=newFetchBuilder().withRateLimit(100,60000,{attempts: 1,initialIntervalMs: 1000,onRejection: (error)=>{if(errorinstanceofRatelimitError){returnerror.remainingTimeInRatelimit;}returnfalse;},}).build();import{FetchBuilder}from'@smooai/fetch';import{z}from'zod';constUserSchema=z.object({id: z.string(),name: z.string(),email: z.string().email(),});// Using the default fetchconstresponse=awaitfetch('https://api.example.com/users/123',{options: {schema: UserSchema,},});// Or using FetchBuilderconstfetch=newFetchBuilder(UserSchema).build();try{constresponse=awaitfetch('https://api.example.com/users/123');// response.data is typed as { id: string; name: string; email: string }}catch(error){if(errorinstanceofHumanReadableSchemaError){console.error('Validation failed:',error.message);// Example output:// Validation failed: Invalid email format at path: email}}import{FetchBuilder}from'@smooai/fetch';constapi=newFetchBuilder().withHooks({// Pre-request hook can modify both URL and request configurationpreRequest: (url,init)=>{constmodifiedUrl=newURL(url.toString());modifiedUrl.searchParams.set('timestamp',Date.now().toString());init.headers={
...init.headers,Authorization: `Bearer ${getToken()}`,};return[modifiedUrl,init];},postResponseSuccess: (url,init,response)=>{metrics.record({endpoint: url.pathname,duration: response.headers.get('x-response-time'),status: response.status,});returnresponse;},postResponseError: (url,init,error)=>{if(error.response?.status===401){refreshToken();// Token expired — refresh and retry}returnerror;},}).build();import{FetchBuilder}from'@smooai/fetch';// Static headers on every requestconstfetch=newFetchBuilder().withInit({headers: {Authorization: 'Bearer your-auth-token','X-API-Key': 'your-api-key',},}).build();// Or a fresh token per request, fetched asynchronouslyconstapi=newFetchBuilder().withAuthTokenProvider(async()=>awaittokenStore.getFreshToken(),'Bearer').build();importfetch,{HTTPResponseError,RatelimitError,RetryError,TimeoutError}from'@smooai/fetch';try{constresponse=awaitfetch('https://api.example.com/data');}catch(error){if(errorinstanceofHTTPResponseError){console.error('HTTP Error:',error.response.status);console.error('Response Data:',error.response.data);}elseif(errorinstanceofRetryError){console.error('Retry failed after all attempts');}elseif(errorinstanceofTimeoutError){console.error('Request timed out');}elseif(errorinstanceofRatelimitError){console.error('Rate limit exceeded');}}- TypeScript · native Fetch API
- Mollitia — circuit breaker and rate limiter (TypeScript port)
- Polly — resilience engine (.NET port)
- Standard Schema
- @smooai/logger — structured logging (bring your own logger supported)
- @smooai/utils — Standard Schema validation and human-readable error generation
@smooai/fetch is built and open-sourced by Smoo AI — the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.
- 🧰 More open source from Smoo AI — smoo.ai/open-source
- 🧩 Sibling packages — @smooai/file, @smooai/logger, @smooai/config, smooth-operator, smooth (the
thCLI)
Contributions are welcome. This project uses changesets to manage versions and releases.
- Fork the repository and create your branch
- Make your changes (the five ports live in
src/,python/,rust/,go/,dotnet/) - Add a changeset to document them:
pnpm changeset - Open a pull request — reference any related issues
MIT © Smoo AI. See LICENSE.
Built by Smoo AI — AI built into every product.
