wretch-logo

Wretch

github-badge npm-badge npm-downloads-badge Coverage Status license-badge

A tiny (~1.8KB g-zipped) wrapper built around fetch with an intuitive syntax.

f[ETCH] [WR]apper

##### Wretch 3.0 is now live πŸŽ‰ ! Check out the [Migration Guide](MIGRATION_V2_V3.md) for upgrading from v2, and please have a look at the [releases](https://github.com/elbywan/wretch/releases) and the [changelog](https://github.com/elbywan/wretch/blob/master/CHANGELOG.md) after each update for new features and breaking changes. ##### And if you like the library please consider becoming a [sponsor](https://github.com/sponsors/elbywan) ❀️. # Features #### `wretch` is a small wrapper around [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) designed to simplify the way to perform network requests and handle responses. - πŸͺΆ **Small** - core is less than 1.8KB g-zipped - πŸ’‘ **Intuitive** - lean API, handles errors, headers and (de)serialization - 🧊 **Immutable** - every call creates a cloned instance that can then be reused safely - πŸ”Œ **Modular** - plug addons to add new features, and middlewares to intercept requests - 🧩 **Isomorphic** - compatible with modern browsers, Node.js 22+, Deno and Bun - 🦺 **Type safe** - strongly typed, written in TypeScript - βœ… **Proven** - fully covered by unit tests and widely used - πŸ’“ **Maintained** - alive and well for many years # Table of Contents - [**Quick Start**](#quick-start) - [**Motivation**](#motivation) - [**Installation**](#installation) - [**Compatibility**](#compatibility) - [**Usage**](#usage) - [**Recipes**](#recipes) - [**Api**](#api-) - [**Addons**](#addons) - [**Middlewares**](#middlewares) - [**Migration Guides**](#migration-guides) - [**License**](#license) # Quick Start ```bash # 1️⃣ Install npm i wretch ``` ```javascript // 2️⃣ Import and create a reusable API client import wretch from "wretch" const api = wretch("https://jsonplaceholder.typicode.com") .options({ mode: "cors" }) // 3️⃣ Make requests with automatic JSON handling const post = await api.get("/posts/1").json() console.log(post.title) // 4️⃣ POST with automatic serialization const created = await api .post({ title: "New Post", body: "Content", userId: 1 }, "/posts") .json() // 5️⃣ Handle errors elegantly await api .get("/posts/999") .notFound(() => console.log("Post not found!")) .json() // 6️⃣ Different response types const text = await api.get("/posts/1").text() // Raw text const response = await api.get("/posts/1").res() // Raw Response object const blob = await api.get("/photos/1").blob() // Binary data ``` # Motivation #### Because having to write a second callback to process a response body feels awkward. Fetch needs a second callback to process the response body. ```javascript fetch("https://jsonplaceholder.typicode.com/posts/1") .then(response => response.json()) .then(json => { //Do stuff with the parsed json }); ``` Wretch does it for you. ```javascript // Use .res for the raw response, .text for raw text, .json for json, .blob for a blob … wretch("https://jsonplaceholder.typicode.com/posts/1") .get() .json(json => { // Do stuff with the parsed json return json }); ``` #### Because manually checking and throwing every request error code is tedious. Fetch won't reject on HTTP error status. ```javascript fetch("https://jsonplaceholder.typicode.com/posts/1") .then(response => { if(!response.ok) { if(response.status === 404) throw new Error("Not found") else if(response.status === 401) throw new Error("Unauthorized") else if(response.status === 418) throw new Error("I'm a teapot !") else throw new Error("Other error") } else {/* … */} }) .then(data => {/* … */}) .catch(error => { /* … */ }) ``` Wretch throws when the response is not successful and contains helper methods to handle common codes. ```javascript wretch("https://jsonplaceholder.typicode.com/posts/1") .get() .notFound(error => { /* … */ }) .unauthorized(error => { /* … */ }) .error(418, error => { /* … */ }) .res(response => {/* … */ }) .catch(error => { /* uncaught errors */ }) ``` #### Because sending a json object should be easy. With fetch you have to set the header, the method and the body manually. ```javascript fetch("https://jsonplaceholder.typicode.com/posts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ "hello": "world" }) }).then(response => {/* … */}) // Omitting the data retrieval and error management parts… ``` With wretch, you have shorthands at your disposal. ```javascript wretch("https://jsonplaceholder.typicode.com/posts") .post({ "hello": "world" }) .res(response => { /* … */ }) ``` #### Because configuration should not rhyme with repetition. A Wretch object is immutable which means that you can reuse previous instances safely. ```javascript const token = "MY_SECRET_TOKEN" // Cross origin authenticated requests on an external API const externalApi = wretch("https://jsonplaceholder.typicode.com") // Base url // Authorization header .auth(`Bearer ${token}`) // Cors fetch options .options({ credentials: "include", mode: "cors" }) // Handle 403 errors .resolve((w) => w.forbidden(error => { /* Handle all 403 errors */ })); // Fetch a resource const resource = await externalApi // Add a custom header for this request .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" }) .get("/posts/1") .json(() => {/* do something with the resource */}); // Post a resource externalApi .url("/posts") .post({ "Shiny new": "resource object" }) .json(() => {/* do something with the created resource */}); ``` # Installation ## Package Manager ```sh npm i wretch # or yarn/pnpm add wretch ``` ## <script> tag The package contains multiple bundles depending on the format and feature set located under the `/dist/bundle` folder.
Bundle variants
> πŸ’‘ If you pick the core bundle, then to plug addons you must import them separately from `/dist/bundle/addons/[addonName].min.js` | Feature set | File Name | | ------------------ | ------------------- | | Core features only | `wretch.min.js` | | Core + all addons | `wretch.all.min.js` | | Format | Extension | | -------- | ---------- | | ESM | `.min.mjs` | | CommonJS | `.min.cjs` | | UMD | `.min.js` |
```html ``` # Compatibility ## Browsers `wretch@^3` is compatible with modern browsers only. For older browsers please use `wretch@^1`. ## Node.js Wretch is compatible with and tested in _Node.js >= 22_. For older Node.js versions, please use `wretch@^2`. Node.js 22+ includes native fetch support and all required Web APIs (FormData, URLSearchParams, AbortController, etc.) out of the box, so no polyfills are needed. ## Deno Works with [Deno](https://deno.land/) out of the box. ```bash deno add npm:wretch ``` ```ts import wretch from "wretch"; const text = await wretch("https://httpbingo.org").get("/status/200").text(); console.log(text); // -> { "code": 200, "description": "OK" } ``` ## Bun Works with [Bun](https://bun.sh/) out of the box. ```bash bun add wretch ``` ```ts import wretch from "wretch"; const text = await wretch("https://httpbingo.org").get("/status/200").text(); console.log(text); // -> { "code": 200, "description": "OK" } ``` # Usage ## Import ```typescript // ECMAScript modules import wretch from "wretch" // CommonJS const wretch = require("wretch") // Global variable (script tag) window.wretch ``` ## Common Use Cases ```javascript // 🌐 REST API Client const api = wretch("https://jsonplaceholder.typicode.com") .auth("Bearer token") .resolve(r => r.json()) const users = await api.get("/users") const user = await api.post({ name: "John" }, "/users") users ``` ```javascript // πŸ“€ File Upload with Progress import ProgressAddon from "wretch/addons/progress" import FormDataAddon from "wretch/addons/formData" await wretch("https://httpbingo.org/post") .addon([FormDataAddon, ProgressAddon()]) .formData({ file: file }) .post() .progress((loaded, total) => console.log(`${(loaded/total*100).toFixed()}%`)) .json() ``` ```javascript // πŸ”„ Auto-retry on Network Failure import { retry } from "wretch/middlewares" const resilientApi = wretch() .middlewares([retry({ maxAttempts: 3, retryOnNetworkError: true })]) ``` ```typescript // 🎯 Type-safe TypeScript interface User { id: number; name: string; email: string } const user = await wretch("https://jsonplaceholder.typicode.com") .get("/users/1") .json() // Fully typed! user ``` ```javascript // πŸ” Automatic Token Refresh const api = wretch("https://httpbingo.org/basic-auth/user/pass") .addon(BasicAuthAddon) .resolve(w => w.unauthorized(async (error, req) => { const newToken = await refreshToken() return req .basicAuth("user", "pass") .unauthorized(e => { console.log("Still unauthorized after token refresh"); throw e }) .fetch() .json() })) ``` ## Custom Fetch Implementation You can provide a custom fetch implementation using the `.fetchPolyfill()` method. This is useful for for a variety of use cases including mocking, adding logging, timing, or other custom behavior to all requests made through a wretch instance. ```js import wretch from "wretch" // Per-instance custom fetch const api = wretch("https://jsonplaceholder.typicode.com") .fetchPolyfill((url, opts) => { console.log('Fetching:', url) console.time(url) return fetch(url, opts).finally(() => { console.timeEnd(url) }) }) await api.get("/posts").json() ``` ### [toFetch() - Fetch Adapter πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#tofetch) Converts a wretch instance into a fetch-like function, preserving all configuration (middlewares, catchers, headers, etc.). Useful for integrating wretch with libraries that expect a fetch signature. ```js const myFetch = wretch("https://jsonplaceholder.typicode.com") .auth("Bearer token") .catcher(401, (err) => console.log("Unauthorized")) .toFetch() // Use like regular fetch const response = await myFetch("/users", { method: "GET" }) response ``` ```js // Pass to libraries expecting fetch import { createClient } from "@apollo/client" const client = createClient({ fetch: wretch().auth("Bearer token").toFetch() }) ``` ## Chaining **A high level overview of the successive steps that can be chained to perform a request and parse the result.** ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Request Chain β”‚ β”‚ β”‚ β”‚ wretch(url) ──> .helper() ──> .body() ──> .httpMethod() β”‚ β”‚ ↓ ↓ ↓ β”‚ β”‚ .headers() .json() .get() β”‚ β”‚ .auth() .body() .post() β”‚ β”‚ .options() .put() β”‚ β”‚ .delete() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ [ fetch() is called ] ↓ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Response Chain β”‚ β”‚ β”‚ β”‚ .catcher() ──> .responseType() ──> Promise ──> .then()/.catch()β”‚ β”‚ ↓ ↓ β”‚ β”‚ .notFound() .json() β”‚ β”‚ .unauthorized() .text() β”‚ β”‚ .error() .blob() β”‚ β”‚ .res() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Step-by-step breakdown:** ```ts // First, instantiate wretch wretch(baseUrl, baseOptions) ``` _The "request" chain starts here._ ```ts // Optional - A set of helper methods to set the default options, set accept header, change the current url… .() // Optional - Serialize an object to json or FormData formats and sets the body & header field if needed .() // Required - Sends the get/put/post/delete/patch request. .() ``` _The "response" chain starts here._ _Fetch is called after the request chain ends and before the response chain starts._
_The request is on the fly and now it is time to chain catchers and finally call a response type handler._ ```ts // Optional - You can chain error handlers here .() // Required - Specify the data type you need, which will be parsed and handed to you .() // >> Ends the response chain. ``` _From this point on, wretch returns a standard Promise._ ```ts .then(…) .catch(…) ``` ### Concrete Example Here's how the chaining works in practice: ```js await wretch("https://api.example.com") // Base URL .headers({ "X-Api-Key": "secret" }) // Helper method .query({ limit: 10 }) // Helper method .json({ name: "Alice", role: "admin" }) // Body type .post("/users") // HTTP method (starts request) .badRequest(err => console.log("Invalid")) // Catcher .unauthorized(err => console.log("No auth")) // Catcher .json(user => console.log(user)) // Response type ``` # Recipes Looking for common patterns and solutions? Check out the **[Recipes Guide](RECIPES.md)** for practical examples covering: - **Error Handling** - Parsing error response bodies, custom error types, global handlers - **TypeScript Patterns** - Typing precomposed instances, reusable catchers - **File Uploads** - Progress tracking, FormData handling - **Query Strings** - Filtering undefined values - **Request Control** - Combining timeouts with AbortControllers, aborting on errors - **Advanced Patterns** - Token refresh & replay, schema validation, async polling # [API πŸ”—](https://elbywan.github.io/wretch/api/) > πŸ’‘ The API documentation is now autogenerated and hosted separately, click the links access it. ### [Factory Method πŸ”—](https://elbywan.github.io/wretch/api/functions/index.default) The default export is a factory function used to instantiate wretch. ```js import wretch from "wretch" const api = wretch("http://domain.com/", { cache: "default" }) ``` ### [Helper Methods πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#accept) Helper Methods are used to configure the request and program actions. **Available methods:** [`.url()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#url) Β· [`.options()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#options) Β· [`.headers()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#headers) Β· [`.auth()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#auth) Β· [`.accept()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#accept) Β· [`.content()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#content) Β· [`.signal()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#signal) Β· [`.toFetch()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#tofetch) Β· [`.fetchPolyfill()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#fetchpolyfill) Β· [`.catcher()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#catcher) Β· [`.catcherFallback()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#catcherfallback) Β· [`.customError()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#customerror) Β· [`.defer()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#defer) Β· [`.resolve()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#resolve) Β· [`.middlewares()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#middlewares) Β· [`.addon()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#addon) Β· [`.polyfills()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#polyfills) ```js let api = wretch("http://domain.com/") api = api .url("/posts/1") .headers({ "Cache-Control": "no-cache" }) .content("text/html") ``` ### [Body Types πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#body) Specify a body type if uploading data. Can also be added through the HTTP Method argument. **Available methods:** [`.body()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#body) Β· [`.json()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#json) ```js let api = wretch("http://domain.com/") api = api.body("
") api ``` ### [HTTP Methods πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#delete) Sets the HTTP method and sends the request. Calling an HTTP method ends the request chain and returns a response chain. You can pass optional url and body arguments to these methods. **Available methods:** [`.get()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#get) Β· [`.post()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#post) Β· [`.put()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#put) Β· [`.patch()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#patch) Β· [`.delete()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#delete) Β· [`.head()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#head) Β· [`.opts()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#opts) ```js const api = wretch("http://jsonplaceholder.typicode.com") // These shorthands: api.get("/posts"); api.post({ json: "body" }, "/posts"); // Are equivalent to: api.url("/posts").get(); api.json({ json: "body" }).url("/posts").post(); ``` **NOTE:** if the body argument is an `Object` it is assumed that it is a JSON payload and it will have the same behaviour as calling `.json(body)` unless the `Content-Type` header has been set to something else beforehand. ### [Catchers πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#badRequest) Catchers are optional, but if none are provided an error will still be thrown for http error codes and it will be up to you to catch it. **Available methods:** [`.badRequest()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#badrequest) Β· [`.unauthorized()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#unauthorized) Β· [`.forbidden()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#forbidden) Β· [`.notFound()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#notfound) Β· [`.timeout()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#timeout) Β· [`.internalError()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#internalerror) Β· [`.error()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#error) Β· [`.fetchError()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#fetcherror) ```js wretch("http://domain.com/resource") .get() .badRequest((err) => console.log(err.status)) .unauthorized((err) => console.log(err.status)) .forbidden((err) => console.log(err.status)) .notFound((err) => console.log(err.status)) .timeout((err) => console.log(err.status)) .internalError((err) => console.log(err.status)) .error(418, (err) => console.log(err.status)) .fetchError((err) => console.log(err)) .res(); ``` The error passed to catchers is enhanced with additional properties. ```ts type WretchError = Error & { status: number; response: WretchResponse; url: string; }; ``` By default, `error.message` is set to the response body text (or `statusText` if body parsing fails). #### Request Replay The original request is passed along the error and can be used in order to perform an additional request. ```js await wretch("https://httpbingo.org/basic-auth/user/pass") .addon(BasicAuthAddon) .basicAuth("user", "wrongpass") .get() .unauthorized(async (error, req) => { // Renew credentials const password = await wretch("https://httpbingo.org/base64/decode/cGFzcw==").get().text(); // Replay the original request with new credentials return req .basicAuth("user", password) .fetch() .unauthorized((err) => { throw err }) .json(); }) .json() // The promise chain is preserved as expected // ".then" will be performed on the result of the original request // or the replayed one (if a 401 error was thrown) .then(() => { /* … */ }); ``` ### [Response Types πŸ”—](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#arrayBuffer) Setting the final response body type ends the chain and returns a regular promise. All these methods accept an optional callback, and will return a Promise resolved with either the return value of the provided callback or the expected type. **Available methods:** [`.res()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#res) Β· [`.json()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#json) Β· [`.text()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#text) Β· [`.blob()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#blob) Β· [`.arrayBuffer()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#arraybuffer) Β· [`.formData()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#formdata) ```js const ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1" // Without a callback wretch(ENDPOINT) .get() .json() .then(json => { /* the json argument is the parsed json of the response body */ }) // Without a callback using await const json = await wretch(ENDPOINT).get().json() // With a callback the value returned is passed to the Promise wretch(ENDPOINT).get().json(json => "Hello world!").then(console.log) // => Hello world! ``` _If an error is caught by catchers, the response type handler will not be called._ # Addons Addons are separate pieces of code that you can import and plug into `wretch` to add new features. ```js import FormDataAddon from "wretch/addons/formData" import QueryStringAddon from "wretch/addons/queryString" // Add both addons const w = wretch().addon([FormDataAddon, QueryStringAddon]) // Additional features are now available w.formData({ hello: "world" }).query({ check: true }) ``` Typescript should also be fully supported and will provide completions. https://user-images.githubusercontent.com/3428394/182319457-504a0856-abdd-4c1d-bd04-df5a061e515d.mov ### [QueryString πŸ”—](https://elbywan.github.io/wretch/api/interfaces/addons_queryString.QueryStringAddon) Used to construct and append the query string part of the URL from an object. ```js import QueryStringAddon from "wretch/addons/queryString" let w = wretch("http://example.com").addon(QueryStringAddon); // url is http://example.com w = w.query({ a: 1, b: 2 }); // url is now http://example.com?a=1&b=2 w = w.query({ c: 3, d: [4, 5] }); // url is now http://example.com?a=1&b=2c=3&d=4&d=5 w = w.query("five&six&seven=eight"); // url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight w = w.query({ reset: true }, { replace: true }); // url is now http://example.com?reset=true ``` ### [FormData πŸ”—](https://elbywan.github.io/wretch/api/interfaces/addons_formData.FormDataAddon) Adds a helper method to serialize a `multipart/form-data` body from an object. ```js import FormDataAddon from "wretch/addons/formData" const form = { duck: "Muscovy", duckProperties: { beak: { color: "yellow", }, legs: 2, }, ignored: { key: 0, }, }; // Will append the following keys to the FormData payload: // "duck", "duckProperties[beak][color]", "duckProperties[legs]" wretch("https://httpbingo.org/post").addon(FormDataAddon).formData(form, { recursive: ["ignored"] }).post(); ``` ### [FormUrl πŸ”—](https://elbywan.github.io/wretch/api/interfaces/addons_formUrl.FormUrlAddon) Adds a method to serialize a `application/x-www-form-urlencoded` body from an object. ```js import FormUrlAddon from "wretch/addons/formUrl" const form = { a: 1, b: { c: 2 } }; const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D"; // Automatically sets the content-type header to "application/x-www-form-urlencoded" wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(form).post(); wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post(); ``` ### [Abort πŸ”—](https://elbywan.github.io/wretch/api/variables/addons.abortAddon) Adds the ability to abort requests and set timeouts using AbortController and signals under the hood. ```js import AbortAddon from "wretch/addons/abort" ``` Use cases : ```js const [c, w] = wretch("https://httpbingo.org/get") .addon(AbortAddon()) .get() .onAbort((_) => console.log("Aborted !")) .controller(); w.text((_) => console.log("should never be called")); c.abort(); // Or : const controller = new AbortController(); wretch("https://httpbingo.org/get") .addon(AbortAddon()) .signal(controller) .get() .onAbort((_) => console.log("Aborted !")) .text((_) => console.log("should never be called")); controller.abort(); ``` ```js await wretch("https://httpbingo.org/delay/2") .addon(AbortAddon()) .get() // 1 second timeout .setTimeout(1000) .onAbort(_ => { console.log("Request timed out") }) .json(_ => { console.log("Response received in time") }) ``` ### [BasicAuth πŸ”—](https://elbywan.github.io/wretch/api/interfaces/addons_basicAuth.BasicAuthAddon) Adds the ability to set the `Authorization` header for the [basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme) without the need to manually encode the username/password. Also, allows using URLs with `wretch` that contain credentials, which would otherwise throw an error. ```js import BasicAuthAddon from "wretch/addons/basicAuth" const user = "user" const pass = "pass" // Automatically sets the Authorization header to "Basic " + wretch("https://httpbingo.org/get") .addon(BasicAuthAddon) .basicAuth(user, pass) .get() // Allows using URLs with credentials in them wretch(`https://${user}:${pass}@httpbingo.org/basic-auth/${user}/${pass}`) .addon(BasicAuthAddon) .get() ``` ### [Progress πŸ”—](https://elbywan.github.io/wretch/api/interfaces/addons_progress.ProgressResolver) Adds the ability to monitor progress when downloading or uploading. _Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._ **Download progress:** ```js import ProgressAddon from "wretch/addons/progress" await wretch("https://httpbingo.org/bytes/5000") .addon(ProgressAddon()) .get() // Called with the number of bytes loaded and the total number of bytes to load .progress((loaded, total) => { console.log(`Download: ${(loaded / total * 100).toFixed(0)}%`) }) .blob() ``` **Upload progress:** ```js import ProgressAddon from "wretch/addons/progress" import FormDataAddon from "wretch/addons/formData" const formData = new FormData() formData.append('file', file) wretch("https://httpbingo.org/post") .addon([ProgressAddon(), FormDataAddon]) .onUpload((loaded, total) => { console.log(`Upload: ${(loaded / total * 100).toFixed(0)}%`) }) .post(formData) .res() ``` > **Note for browsers:** Upload progress requires HTTPS (HTTP/2) in Chrome/Chromium and doesn't work in Firefox due to streaming limitations. Works fully in Node.js. ### [Performance πŸ”—](https://elbywan.github.io/wretch/api/variables/addons.perfsAddon) Adds the ability to measure requests using the Performance Timings API. Uses the Performance API (browsers & Node.js) to expose timings related to the underlying request. > πŸ’‘ Make sure to follow the additional instructions in the documentation to setup Node.js if necessary. # Middlewares Middlewares are functions that can intercept requests before being processed by Fetch. Wretch includes a helper to help replicate the [middleware](http://expressjs.com/en/guide/using-middleware.html) style. ```js import wretch from "wretch" import { retry, dedupe } from "wretch/middlewares" const w = wretch().middlewares([retry(), dedupe()]) ``` > πŸ’‘ The following middlewares were previously provided by the [`wretch-middlewares`](https://github.com/elbywan/wretch-middlewares/) package. ### [Retry πŸ”—](https://elbywan.github.io/wretch/api/types/middlewares_retry.RetryMiddleware) **Retries a request multiple times in case of an error (or until a custom condition is true).** > **πŸ’‘ By default, the request will be retried only for server errors (5xx) and other non-successful responses, but not for client errors (4xx).** > > ```js > // To retry on all non-2xx responses (including 4xx): > until: (response, error) => !!response && response.ok > ``` ```js import wretch from 'wretch' import { retry } from 'wretch/middlewares' wretch().middlewares([ retry({ /* Options - defaults below */ delayTimer: 500, delayRamp: (delay, nbOfAttempts) => delay * nbOfAttempts, maxAttempts: 10, until: (response, error) => !!response && (response.ok || (response.status >= 400 && response.status < 500)), onRetry: undefined, retryOnNetworkError: false, resolveWithLatestResponse: false }) ]) // You can also return a Promise, which is useful if you want to inspect the body: wretch().middlewares([ retry({ until: response => response?.clone().json().then(body => body.field === 'something' ) || false }) ]) ``` ### [Dedupe πŸ”—](https://elbywan.github.io/wretch/api/types/middlewares_dedupe.DedupeMiddleware) **Prevents having multiple identical requests on the fly at the same time.** ```js import wretch from 'wretch' import { dedupe } from 'wretch/middlewares' wretch().middlewares([ dedupe({ /* Options - defaults below */ skip: (url, opts) => opts.skipDedupe || opts.method !== 'GET', key: (url, opts) => opts.method + '@' + url, resolver: response => response.clone() }) ]) ``` ### [Throttling Cache πŸ”—](https://elbywan.github.io/wretch/api/types/middlewares_throttlingCache.ThrottlingCacheMiddleware) **A throttling cache which stores and serves server responses for a certain amount of time.** ```js import wretch from 'wretch' import { throttlingCache } from 'wretch/middlewares' wretch().middlewares([ throttlingCache({ /* Options - defaults below */ throttle: 1000, skip: (url, opts) => opts.skipCache || opts.method !== 'GET', key: (url, opts) => opts.method + '@' + url, clear: (url, opts) => false, invalidate: (url, opts) => null, condition: response => response.ok, flagResponseOnCacheHit: '__cached' }) ]) ``` ### [Delay πŸ”—](https://elbywan.github.io/wretch/api/types/middlewares_delay.DelayMiddleware) **Delays the request by a specific amount of time.** ```js import wretch from 'wretch' import { delay } from 'wretch/middlewares' wretch().middlewares([ delay(1000) ]) ``` ## Writing a Middleware Basically a Middleware is a function having the following signature : ```ts // A middleware accepts options and returns a configured version type Middleware = (options?: { [key: string]: any }) => ConfiguredMiddleware; // A configured middleware (with options curried) type ConfiguredMiddleware = (next: FetchLike) => FetchLike; // A "fetch like" function, accepting an url and fetch options and returning a response promise type FetchLike = ( url: string, opts: WretchOptions, ) => Promise; ``` ### Context If you need to manipulate data within your middleware and expose it for later consumption, a solution could be to pass a named property to the wretch options (_suggested name: `context`_). Your middleware can then take advantage of that by mutating the object reference. ```js const contextMiddleware = (next) => (url, opts) => { if (opts.context) { // Mutate "context" opts.context.property = "anything"; } return next(url, opts); }; // Provide the reference to a "context" object const context = {}; const res = await wretch("https://httpbingo.org/get") // Pass "context" by reference as an option .options({ context }) .middlewares([contextMiddleware]) .get() .res(); console.log(context.property); // prints "anything" ``` ### Advanced examples
 πŸ‘€ Show me the code
```javascript /* A simple delay middleware. */ const delayMiddleware = delay => next => (url, opts) => { return new Promise(res => setTimeout(() => res(next(url, opts)), delay)) } /* Returns the url and method without performing an actual request. */ const shortCircuitMiddleware = () => next => (url, opts) => { // We create a new Response object to comply because wretch expects that from fetch. const response = new Response(url) // Instead of calling next(), returning a Response Promise bypasses the rest of the chain. return Promise.resolve(response) } /* Logs all requests passing through. */ const logMiddleware = () => next => (url, opts) => { console.log(opts.method + "@" + url) return next(url, opts) } /* A throttling cache. */ const cacheMiddleware = (throttle = 0) => { const cache = new Map() const inflight = new Map() const throttling = new Set() return next => (url, opts) => { const key = opts.method + "@" + url if(!opts.noCache && throttling.has(key)) { // If the cache contains a previous response and we are throttling, serve it and bypass the chain. if(cache.has(key)) return Promise.resolve(cache.get(key).clone()) // If the request in already in-flight, wait until it is resolved else if(inflight.has(key)) { return new Promise((resolve, reject) => { inflight.get(key).push([resolve, reject]) }) } } // Init. the pending promises Map if(!inflight.has(key)) inflight.set(key, []) // If we are not throttling, activate the throttle for X milliseconds if(throttle && !throttling.has(key)) { throttling.add(key) setTimeout(() => { throttling.delete(key) }, throttle) } // We call the next middleware in the chain. return next(url, opts) .then(_ => { // Add a cloned response to the cache cache.set(key, _.clone()) // Resolve pending promises inflight.get(key)?.forEach((([resolve, reject]) => resolve(_.clone()))) // Remove the inflight pending promises inflight.delete(key) // Return the original response return _ }) .catch(_ => { // Reject pending promises on error inflight.get(key)?.forEach(([resolve, reject]) => reject(_)) inflight.delete(key) throw _ }) } } // To call a single middleware const cache = cacheMiddleware(1000) wretch("https://httpbingo.org/get").middlewares([cache]).get() // To chain middlewares wretch("https://httpbingo.org/get").middlewares([ logMiddleware(), delayMiddleware(1000), shortCircuitMiddleware() ]).get().text(text => console.log(text)) // To test the cache middleware more thoroughly const wretchCache = wretch("https://httpbingo.org").middlewares([cacheMiddleware(500)]) const printResource = (url, timeout = 0) => { return new Promise(resolve => setTimeout(async () => { wretchCache.url(url).get().notFound(console.error).text(resource => { console.log(resource) resolve(resource) }) }, timeout)) } // The resource url, change it to an invalid route to check the error handling const resourceUrl = "/base64/decode/YWVhY2YyYWYtODhlNi00ZjgxLWEwYjAtNzdhMTIxNTA0Y2E4" // Only two actual requests are made here even though there are 30 calls await Promise.all(Array.from({ length: 10 }).flatMap(() => [ printResource(resourceUrl), printResource(resourceUrl, 200), printResource(resourceUrl, 700) ] )) ```
# Limitations ## [Cloudflare Workers](https://workers.cloudflare.com/) It seems like using `wretch` in a Cloudflare Worker environment is not possible out of the box, as the Cloudflare `Response` implementation does not implement the [`type`](https://developer.mozilla.org/en-US/docs/Web/API/Response/type) property and throws an error when trying to access it. #### Please check the issue [#159](https://github.com/elbywan/wretch/issues/159) for more information. ### Workaround The following middleware should fix the issue (thanks @jimmed πŸ™‡): ```js wretch().middlewares([ (next) => async (url, opts) => { const response = await next(url, opts); try { Reflect.get(response, "type", response); } catch (error) { Object.defineProperty(response, "type", { get: () => "default", }); } return response; }, ]) ``` ## Headers Case Sensitivity The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from the Fetch API uses the [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) class to store headers under the hood. This class is case-insensitive, meaning that setting both will actually appends the value to the same key: ```js const headers = new Headers(); headers.append("Accept", "application/json"); headers.append("accept", "application/json"); headers.forEach((value, key) => console.log(key, value)); // prints: accept application/json, application/json ``` When using `wretch`, please be mindful of this limitation and avoid setting the same header multiple times with a different case: ```js wretch("https://httpbingo.org/post") .headers({ "content-type": "application/json" }) // .json is a shortcut for .headers("Content-Type": "application/json").post().json() .json({ foo: "bar" }) // Wretch stores the headers inside a plain javascript object and will not deduplicate them. // Later on when fetch builds the Headers object the content type header will be set twice // and its value will be "application/json, application/json". // Ultimately this is certainly not what you want. ``` #### Please check the issue [#80](https://github.com/elbywan/wretch/issues/80) for more information. ### Workaround You can use the following middleware to deduplicate headers (thanks @jimmed πŸ™‡): ```js export const manipulateHeaders = callback => next => (url, { headers, ...opts }) => { const nextHeaders = callback(new Headers(headers)) return next(url, { ...opts, headers: nextHeaders }) } export const dedupeHeaders = (dedupeHeaderLogic = {}) => { const deduperMap = new Map( Object.entries(dedupeHeaderLogic).map(([k, v]) => [k.toLowerCase(), v]), ) const dedupe = key => deduperMap.get(key.toLowerCase()) ?? (values => new Set(values)) return manipulateHeaders((headers) => { Object.entries(headers.raw()).forEach(([key, values]) => { const deduped = Array.from(dedupe(key)(values)) headers.delete(key) deduped.forEach((value, index) => headers[index ? 'append' : 'set'](key.toLowerCase(), value), ) }) return headers }) } // By default, it will deduplicate identical values for a given header. This can be used as follows: wretch().middlewares([dedupeHeaders()]) // If there is a specific header for which the defaults cause problems, then you can provide a callback to handle deduplication yourself: wretch().middlewares([ dedupeHeaders({ Accept: (values) => values.filter(v => v !== '*/*') }) ]) ``` # Migration Guides Comprehensive migration guides are available for upgrading between major versions: - **[Migration Guide: v2 to v3](MIGRATION_V2_V3.md)** - **[Migration Guide: v1 to v2](MIGRATION_V1_V2.md)** # License MIT