SmooAI.Fetch
4.0.0
dotnet add package SmooAI.Fetch --version 4.0.0
NuGet\Install-Package SmooAI.Fetch -Version 4.0.0
<PackageReference Include="SmooAI.Fetch" Version="4.0.0" />
<PackageVersion Include="SmooAI.Fetch" Version="4.0.0" />
<PackageReference Include="SmooAI.Fetch" />
paket add SmooAI.Fetch --version 4.0.0
#r "nuget: SmooAI.Fetch, 4.0.0"
#:package SmooAI.Fetch@4.0.0
#addin nuget:?package=SmooAI.Fetch&version=4.0.0
#tool nuget:?package=SmooAI.Fetch&version=4.0.0
SmooAI.Fetch
HTTP that gets out of your way for .NET 8+ — typed JSON in and out, automatic retry on transient failures, auth token injection, one error type for every non-2xx.
.NET port of @smooai/fetch. Stop writing the same retry/backoff/auth wrapper for every API client in every service. Wire-compatible semantics with the TypeScript, Python, Go, and Rust ports.
Install
dotnet add package SmooAI.Fetch
What you get
- Typed JSON —
GetAsync<User>andPostAsync<Dto, User>. Your request and response shapes, strongly typed. NoJsonSerializer.Deserializeboilerplate on every call site. - Automatic retries on transient failures, for requests that are safe to replay — network blips, timeouts, and
408/429/5xxresponses are retried with exponential backoff + jitter for idempotent methods (GET,HEAD,OPTIONS,TRACE,PUT,DELETE).POSTandPATCHare never retried unless you opt in — see Retries are method-aware. Retry-Afteris honored — when a server tells you "wait 5s", the client waits 5s instead of your default backoff. Never eat a 429 again.- Async auth tokens — register an
AuthTokenProvideronce; every request picks up a fresh bearer token without restartingHttpClient. - One typed error per non-2xx — catch
HttpResponseErrorand you've got status, body, headers, URI, and method on one exception. - Per-request cancellation + timeout — linked
CancellationTokenSourceunder the hood; every method takes aCancellationToken. - DI-ready —
AddSmooFetch(options => …)plugs intoIHttpClientFactoryand yourIServiceCollection.
Quick start — standalone
using SmooAI.Fetch;
var fetch = SmooFetch.Create(options =>
{
options.BaseUrl = "https://api.example.com";
options.Timeout = TimeSpan.FromSeconds(30);
options.RetryPolicy = RetryPolicy.ExponentialBackoff(maxRetries: 3);
options.AuthTokenProvider = async ct => await GetBearerTokenAsync(ct);
});
var me = await fetch.GetAsync<User>("/users/me");
var created = await fetch.PostAsync<CreateUserDto, User>("/users", dto);
Quick start — DI / IHttpClientFactory
builder.Services.AddSmooFetch(options =>
{
options.BaseUrl = builder.Configuration["Api:BaseUrl"];
options.RetryPolicy = RetryPolicy.ExponentialBackoff(maxRetries: 3);
options.AuthTokenProvider = _ => Task.FromResult<string?>(bearerToken);
});
// Inject wherever you need it
public class BillingService(SmooFetch fetch)
{
public Task<Invoice> GetInvoice(string id) =>
fetch.GetAsync<Invoice>($"/invoices/{id}");
}
Retry policy — honors Retry-After
options.RetryPolicy = RetryPolicy.ExponentialBackoff(
maxRetries: 3,
baseDelay: TimeSpan.FromMilliseconds(250),
maxDelay: TimeSpan.FromSeconds(10));
- Retries on transient exceptions (timeouts, socket errors) and on
408/429/500/502/503/504— for retry-eligible requests only (below). - Honors the
Retry-Afterheader on429/503— if the server says "wait 5s", the client waits 5s instead of your backoff. - Exponential backoff with jitter; bounded by
maxDelay.
Retries are method-aware
A POST that timed out, or came back 429 / 5xx, may already have run on the server. Sending it again can charge a card twice, send a message twice, or bill a second generated image. So a failed attempt is retried only when the request is retry-eligible:
- its method is idempotent per RFC 9110 §9.2.2 —
GET,HEAD,OPTIONS,TRACE,PUT,DELETE; or - the policy sets
AllowNonIdempotent = true; or - the request carries a non-empty
Idempotency-Keyheader (RetryPolicy.IdempotencyKeyHeader), i.e. the server has promised to de-duplicate replays.
An ineligible request makes exactly one attempt. OnRejection is not consulted, and the outcome surfaces as-is: the non-2xx response or the original exception. That includes a 429 with Retry-After on a POST: it is still not replayed unless you opt in. Eligibility is decided on the final request, after the AuthTokenProvider and PreRequest hook run, so a hook can add the Idempotency-Key. The in-process rate limiter is unaffected, because it waits before anything is sent.
// This endpoint is safe to replay: opt the whole client in.
options.RetryPolicy = RetryPolicy.Default with { AllowNonIdempotent = true };
// Or opt in one request, with a key the server de-duplicates on.
using var request = new HttpRequestMessage(HttpMethod.Post, "/payments") { Content = body };
request.Headers.Add(RetryPolicy.IdempotencyKeyHeader, paymentId);
using var response = await fetch.SendAsync(request);
Changed in 4.0.0. Through 3.x every method was retried,
POSTincluded.
Typed errors — one catch per layer
try
{
var user = await fetch.GetAsync<User>("/users/me");
}
catch (HttpResponseError ex)
{
// ex.StatusCode — int
// ex.Body — string (response body)
// ex.Headers — HttpResponseHeaders
// ex.RequestUri — Uri
// ex.Method — HttpMethod
}
HttpResponseError is thrown for every non-2xx response. Transient exceptions (timeouts, socket resets) surface as their original type if retries are exhausted.
Async auth token provider
Auth is fetched per request via AuthTokenProvider: async ct => … — pair this with a cached/rotating token source and every call gets a fresh Authorization header without re-registering the client.
options.AuthTokenProvider = async ct =>
{
var token = await _tokenCache.GetOrRefreshAsync(ct);
return token; // null => no Authorization header on this request
};
Cancellation + per-request timeout
Every method accepts a CancellationToken. The configured Timeout applies per attempt: each attempt runs under its own CancellationTokenSource, linked to yours. When the timeout fires, HttpClient aborts the in-flight request and closes its connection before any retry starts, so the server sees the cancel. A timed-out attempt is never left running alongside its retry.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var user = await fetch.GetAsync<User>("/users/me", cancellationToken: cts.Token);
Related
@smooai/fetch— TypeScript / Nodesmooai-fetch— Rustsmooai-fetch— Pythongithub.com/SmooAI/fetch/go/fetch/v3— Go
License
MIT — © SmooAI
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Http.Polly (>= 8.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Polly (>= 8.5.0)
- System.Threading.RateLimiting (>= 8.0.0)
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Http.Polly (>= 8.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Polly (>= 8.5.0)
- System.Threading.RateLimiting (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Http.Polly (>= 8.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
- Polly (>= 8.5.0)
- System.Threading.RateLimiting (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.0 | 34 | 9/27/2026 |
| 3.7.1 | 108 | 8/28/2026 |
| 3.7.0 | 96 | 8/28/2026 |
| 3.6.2 | 140 | 8/20/2026 |
| 3.6.1 | 96 | 8/20/2026 |
| 3.6.0 | 98 | 8/20/2026 |
| 3.5.1 | 100 | 8/20/2026 |
| 3.5.0 | 103 | 8/20/2026 |
| 3.4.2 | 99 | 8/20/2026 |
| 3.4.1 | 113 | 8/20/2026 |
| 3.4.0 | 133 | 8/15/2026 |
| 3.3.10 | 146 | 5/18/2026 |
| 3.3.9 | 119 | 5/12/2026 |
| 3.3.8 | 107 | 5/12/2026 |
| 3.3.7 | 108 | 5/12/2026 |
| 3.3.6 | 118 | 5/12/2026 |
| 3.3.5 | 116 | 5/12/2026 |
| 3.3.4 | 117 | 4/24/2026 |