A tiny wrapper built around fetch with an intuitive syntax. :candy:
4.83k
stars
98
forks
source link
A tiny (~2KB g-zipped) wrapper built around fetch with an intuitive syntax.
f[ETCH] [WR]apper
##### Wretch 2.11 is now live π ! 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. If you want to try out the hot stuff, please look into the [dev](https://github.com/elbywan/wretch/tree/dev) branch.
##### 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 2KB 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 14+ and Deno
- π¦Ί **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
- [**Motivation**](#motivation)
- [**Installation**](#installation)
- [**Compatibility**](#compatibility)
- [**Usage**](#usage)
- [**Api**](#api-)
- [**Addons**](#addons)
- [**Middlewares**](#middlewares)
- [**Migration from v1**](#migration-from-v1)
- [**License**](#license)
# 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("examples/example.json")
.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("examples/example.json")
.get()
.json(json => {
// Do stuff with the parsed json
});
```
#### Because manually checking and throwing every request error code is tedious.
Fetch wonβt reject on HTTP error status.
```javascript
fetch("anything")
.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("anything")
.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("endpoint", {
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("endpoint")
.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
// Cross origin authenticated requests on an external API
const externalApi = wretch("http://external.api") // Base url
// Authorization header
.auth(`Bearer ${token}`)
// Cors fetch options
.options({ credentials: "include", mode: "cors" })
// Handle 403 errors
.resolve((_) => _.forbidden(handle403));
// 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("/resource/1")
.json(handleResource);
// Post a resource
externalApi
.url("/resource")
.post({ "Shiny new": "resource object" })
.json(handleNewResourceResult);
```
# 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@^2` is compatible with modern browsers only. For older browsers please use `wretch@^1`.
## Node.js
Wretch is compatible with and tested in _Node.js >= 14_. Older versions of node may work
but it is not guaranteed.
### Polyfills (Node.js < 18)
**Starting from Node.js 18, [node includes experimental fetch support](https://nodejs.org/en/blog/announcements/v18-release-announce/). Wretch will work without installing any polyfill.**
For older versions, the Node.js standard library does not provide a native implementation of fetch (and other Browsers-only APIs) and polyfilling is mandatory.
_The non-global way (preferred):_
```javascript
import fetch, { FormData } from "node-fetch"
// w is a reusable wretch instance
const w = wretch().polyfills({
fetch,
FormData,
});
```
_Globally:_
```javascript
import fetch, { FormData } from "node-fetch";
// Either mutate the global objectβ¦
global.fetch = fetch;
global.FormData = FormData;
// β¦or use the static wretch.polyfills method to impact every wretch instance created afterwards.
wretch.polyfills({
fetch,
FormData,
});
```
## Deno
Works with [Deno](https://deno.land/) >=
[0.41.0](https://github.com/denoland/deno/releases/tag/v0.41.0) out of the box.
Types should be imported from `/dist/types.d.ts`.
```ts
// You can import wretch from any CDN that serve ESModules.
import wretch from "https://cdn.skypack.dev/wretch";
const text = await wretch("https://httpstat.us").get("/200").text();
console.log(text); // -> 200 OK
```
# Usage
## Import
```typescript
// ECMAScript modules
import wretch from "wretch"
// CommonJS
const wretch = require("wretch")
// Global variable (script tag)
window.wretch
```
## Minimal Example
```js
import wretch from "wretch"
// Instantiate and configure wretch
const api =
wretch("https://jsonplaceholder.typicode.com", { mode: "cors" })
.errorType("json")
.resolve(r => r.json())
try {
// Fetch users
const users = await api.get("/users")
// Find all posts from a given user
const user = users.find(({ name }) => name === "Nicholas Runolfsdottir V")
const postsByUser = await api.get(`/posts?userId=${user.id}`)
// Create a new post
const newPost = await api.url("/posts").post({
title: "New Post",
body: "My shiny new post"
})
// Patch it
await api.url("/posts/" + newPost.id).patch({
title: "Updated Post",
body: "Edited body"
})
// Fetch it
await api.get("/posts/" + newPost.id)
} catch (error) {
// The API could return an empty object - in which case the status text is logged instead.
const message =
typeof error.message === "object" && Object.keys(error.message).length > 0
? JSON.stringify(error.message)
: error.response.statusText
console.error(`${error.status}: ${message}`)
}
```
## Chaining
**A high level overview of the successive steps that can be chained to perform a request and parse the result.**
```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(β¦)
```
# [API π](https://elbywan.github.io/wretch/api)
> π‘ The API documentation is now autogenerated and hosted separately, click the links access it.
### [Static Methods π](https://elbywan.github.io/wretch/api/functions/index.default)
These methods are available from the main default export and can be used to instantiate wretch and configure it globally.
```js
import wretch from "wretch"
wretch.options({ mode: "cors" })
let w = 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.
```js
w = w
.url("/resource/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.
```js
w = w.body("")
```
### [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.
```js
// These shorthands:
wretch().get("/url");
wretch().post({ json: "body" }, "/url");
// Are equivalent to:
wretch().url("/url").get();
wretch().json({ json: "body" }).url("/url").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.
```js
wretch("...")
.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;
text?: string;
json?: Object;
};
```
The original request is passed along the error and can be used in order to
perform an additional request.
```js
wretch("/resource")
.get()
.unauthorized(async (error, req) => {
// Renew credentials
const token = await wretch("/renewtoken").get().text();
storeToken(token);
// Replay the original request with new credentials
return req.auth(token).get().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(callback);
```
### [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.
```js
// Without a callback
wretch("...").get().json().then(json => /* json is the parsed json of the response body */)
// Without a callback using await
const json = await wretch("...").get().json()
// With a callback the value returned is passed to the Promise
wretch("...").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).addon(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 }, 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("...").addon(FormDataAddon).formData(form, ["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("...").addon(FormUrlAddon).formUrl(form).post();
wretch("...").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post();
```
### [Abort π](https://elbywan.github.io/wretch/api/functions/addons_abort.default)
Adds the ability to abort requests and set timeouts using AbortController and signals under the hood.
```js
import AbortAddon from "wretch/addons/abort"
```
_Only compatible with browsers that support
[AbortControllers](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
Otherwise, you could use a (partial)
[polyfill](https://www.npmjs.com/package/abortcontroller-polyfill)._
Use cases :
```js
const [c, w] = wretch("...")
.addon(AbortAddon())
.get()
.onAbort((_) => console.log("Aborted !"))
.controller();
w.text((_) => console.log("should never be called"));
c.abort();
// Or :
const controller = new AbortController();
wretch("...")
.addon(AbortAddon())
.signal(controller)
.get()
.onAbort((_) => console.log("Aborted !"))
.text((_) => console.log("should never be called"));
controller.abort();
```
```js
// 1 second timeout
wretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_ =>
// will not be called if the request timeouts
)
```
### [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("...").addon(BasicAuthAddon).basicAuth(user, pass).get()
// Allows using URLs with credentials in them
wretch(`https://${user}:${pass}@...`).addon(BasicAuthAddon).get()
```
### [Progress π](https://elbywan.github.io/wretch/api/interfaces/addons_progress.ProgressResolver)
Adds the ability to monitor progress when downloading a response.
_Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._
```js
import ProgressAddon from "wretch/addons/progress"
wretch("some_url")
.addon(ProgressAddon())
.get()
// Called with the number of bytes loaded and the total number of bytes to load
.progress((loaded, total) => {
console.log(`${(loaded / total * 100).toFixed(0)}%`)
})
.text()
```
### [Performance π](https://elbywan.github.io/wretch/api/functions/addons_perfs.default)
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 if the response status is not in the 2xx range.**
>
> ```js
> // Replace the default condition with a custom one to avoid retrying on 4xx errors:
> until: (response, error) => response && (response.ok || (response.status >= 400 && response.status < 500))
> ```
```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,
onRetry: null,
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'
)
})
])
```
### [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("...")
// 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()
response.text = () => Promise.resolve(opts.method + "@" + url)
response.json = () => Promise.resolve({ url, method: opts.method })
// 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("...").middlewares([cache]).get()
// To chain middlewares
wretch("...").middlewares([
logMiddleware(),
delayMiddleware(1000),
shortCircuitMiddleware()
}).get().text(_ => console.log(text))
// To test the cache middleware more thoroughly
const wretchCache = wretch().middlewares([cacheMiddleware(1000)])
const printResource = (url, timeout = 0) =>
setTimeout(_ => wretchCache.url(url).get().notFound(console.error).text(console.log), timeout)
// The resource url, change it to an invalid route to check the error handling
const resourceUrl = "/"
// Only two actual requests are made here even though there are 30 calls
for(let i = 0; i < 10; i++) {
printResource(resourceUrl)
printResource(resourceUrl, 500)
printResource(resourceUrl, 1500)
}
```
# 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(url)
.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 from v1
## Philosophy
Wretch has been **completely rewritten** with the following goals in mind:
- reduce its size by making it modular
- preserve the typescript type coverage
- improve the API by removing several awkward choices
## Compatibility
`wretch@1` was transpiled to es5, `wretch@2` is now transpiled to es2018.
Any "modern" browser and Node.js versions >= 14 should parse the library without issues.
If you need compatibility with older browsers/nodejs versions then either stick with v1, use poyfills
or configure `@babel` to make it transpile wretch.
## Addons
Some features that were part of `wretch` v1 are now split apart and must be imported through addons.
It is now needed to pass the Addon to the [`.addon`](#addonaddon-wretchaddon) method to register it.
Please refer to the [Addons](#addons) documentation.
```js
/* Previously (wretch@1) */
import wretch from "wretch"
wretch.formData({ hello: "world" }).query({ check: true })
/* Now (wretch@2) */
import FormDataAddon from "wretch/addons/formData"
import QueryStringAddon from "wretch/addons/queryString"
import baseWretch from "wretch"
// Add both addons
const wretch = baseWretch().addon(FormDataAddon).addon(QueryStringAddon)
// Additional features are now available
wretch.formData({ hello: "world" }).query({ check: true })
```
## Typescript
Types have been renamed and refactored, please update your imports accordingly and refer to the [typescript api documentation](https://elbywan.github.io/wretch/api).
## API Changes
### Replace / Mixin arguments
Some functions used to have a `mixin = true` argument that could be used to merge the value, others a `replace = false` argument performing the opposite.
In v2 there are only `replace = false` arguments but the default behaviour should be preserved.
```js
/* Previously (wretch@1) */
wretch.options({ credentials: "same-origin" }, false) // false: do not merge the value
wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
/* Now (wretch@2) */
wretch.options({ credentials: "same-origin" }, true) // true: replace the existing value
wretch.options({ credentials: "same-origin" }) // Default behaviour stays the same
```
### HTTP methods extra argument
In v1 it was possible to set fetch options while calling the http methods to end the request chain.
```js
/* Previously (wretch@1) */
wretch("...").get({ my: "option" })
```
This was a rarely used feature and the extra argument now appends a string to the base url.
```js
/* Now (wretch@2) */
wretch("https://base.com").get("/resource/1")
```
### Replay function
The `.replay` function has been renamed to [`.fetch`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#fetch).
# License
MIT