A tiny wrapper built around fetch with an intuitive syntax. :candy:

Overview

wretch-logo

Wretch

travis-badge npm-badge npm-downloads-badge Coverage Status license-badge

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

f[ETCH] [WR]apper

Wretch 1.7 is now live πŸŽ‰ ! Please check out the changelog after each update for new features and breaking changes. If you want to try out the hot stuff, please look into the dev branch.
A collection of middlewares is available through the wretch-middlewares package! πŸ“¦

Table of Contents

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

fetch("examples/example.json")
  .then(response => response.json())
  .then(json => {
    //Do stuff with the parsed json
  })
// Wretch does it for you

// 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

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

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

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

wretch("endpoint")
  .post({ "hello": "world" })
  .res(response => /* ... */)

Because configuration should not rhyme with repetition.

// Wretch object is immutable which means that you can configure, store and reuse instances

// Cross origin authenticated requests on an external API
const externalApi = wretch()
  // Set the base url
  .url("http://external.api")
  // Authorization header
  .auth(`Bearer ${ token }`)
  // Cors fetch options
  .options({ credentials: "include", mode: "cors" })
  // Handle 403 errors
  .resolve(_ => _.forbidden(handle403))

// Fetch a resource
externalApi
  .url("/resource/1")
  // Add a custom header for this request
  .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
  .get()
  .json(handleResource)
// Post a resource
externalApi
  .url("/resource")
  .post({ "Shiny new": "resource object" })
  .json(handleNewResourceResult)

Installation

Npm

npm i wretch

Clone

git clone https://github.com/elbywan/wretch
cd wretch
npm install
npm start

Compatibility

Browsers

Wretch is compatible with modern browsers out of the box.

For older environments without fetch support, you should get a polyfill.

Node.js

Works with any FormData or fetch polyfills.

// The global way :

global.fetch = require("node-fetch")
global.FormData = require("form-data")
global.URLSearchParams = require("url").URLSearchParams

// Or the non-global way :

wretch().polyfills({
    fetch: require("node-fetch"),
    FormData: require("form-data"),
    URLSearchParams: require("url").URLSearchParams
})

Deno

Works with Deno >= 0.41.0 out of the box.

// You can import wretch from any CDN that serve ESModules.
import wretch from 'https://cdn.pika.dev/wretch'

const text = await wretch('https://httpstat.us/200').get().text()
console.log(text) // -> 200 OK

This project uses automated node.js & browser unit tests. The latter are a provided courtesy of:

browserstack-logo

Usage

Wretch is bundled using the UMD format (@dist/bundle/wretch.js), ESM format (@dist/bundle/wretch.esm.js) alongside es2015 modules (@dist/index.js) and typescript definitions.

Import

<script> tag

<!--
  Pick your favourite CDN:
    - https://unpkg.com
    - https://www.jsdelivr.com/package/npm/wretch
    - https://www.skypack.dev/view/wretch
    - https://cdnjs.com/libraries/wretch
-->

<!-- UMD import as window.wretch -->
<script src="https://unpkg.com/wretch"></script>

<!-- Modern import -->
<script type="module">
  import wretch from 'https://cdn.skypack.dev/wretch'

  // ... //
</script>

ESModule

import wretch from "wretch"

CommonJS

const wretch = require("wretch")

Code

Wretcher objects are immutable.

wretch(url, options)

  /* The "request" chain. */

 .[helper method(s)]()
     // [ Optional ]
     // A set of helper methods to set the default options, set accept header, change the current url ...
 .[body type]()
     // [ Optional ]
     // Serialize an object to json or FormData formats and sets the body & header field if needed
 .[http method]()
     // [ Required, ends the request chain ]
     // Performs the get/put/post/delete/patch request

  /* Fetch is called at this time. */
  /* The request is sent, and from this point on you can chain catchers and call a response type handler. */

  /* The "response" chain. */

 .[catcher(s)]()
    // [ Optional ]
    // You can chain error handlers here
 .[response type]()
    // [ Required, ends the response chain ]
    // Specify the data type you need, which will be parsed and handed to you

  /* From this point wretch returns a standard Promise, so you can continue chaining actions afterwards. */

  .then(/* ... */)
  .catch(/* ... */)

API


wretch(url = "", opts = {})

Creates a new Wretcher object with an url and vanilla fetch options.

Helper Methods

Helper methods are optional and can be chained.

url query options headers accept content auth catcher resolve defer defaults errorType polyfills

url(url: string, replace: boolean = false)

Appends or replaces the url.

wretch().url("...").get().json(/* ... */)

// Can be used to set a base url

// Subsequent requests made using the 'blogs' object will be prefixed with "http://mywebsite.org/api/blogs"
const blogs = wretch("http://mywebsite.org/api/blogs")

// Perfect for CRUD apis
const id = await blogs.post({ name: "my blog" }).json(_ => _.id)
const blog = await blogs.url(`/${id}`).get().json()
console.log(blog.name)

await blogs.url(`/${id}`).delete().res()

// And to replace the base url if needed :
const noMoreBlogs = blogs.url("http://mywebsite.org/", true)

query(qp: object | string, replace: boolean)

Converts a javascript object to query parameters, then appends this query string to the current url. String values are used as the query string verbatim.

Pass true as the second argument to replace existing query parameters.

let w = wretch("http://example.com")
// 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
Note that .query is not meant to handle complex cases with nested objects.

For this kind of usage, you can use wretch in conjunction with other libraries (like qs).

/* Using wretch with qs */

const queryObject = { some: { nested: 'objects' }}

// Use .qs inside .query :

 wretch("https://example.com/").query(qs.stringify(queryObject))

// Use .defer :

const qsWretch = wretch().defer((w, url, { qsQuery, qsOptions }) => (
    qsQuery ? w.query(qs.stringify(qsQuery, qsOptions)) : w
))

qsWretch
  .url("https://example.com/")
  .options({ qs: { query: queryObject }})
  /* ... */

options(options: Object, mixin: boolean = true)

Sets the fetch options.

wretch("...").options({ credentials: "same-origin" })

Wretch being immutable, you can store the object for later use.

const corsWretch = wretch().options({ credentials: "include", mode: "cors" })

corsWretch.url("http://endpoint1").get()
corsWretch.url("http://endpoint2").get()

You can override instead of mixing in the existing options by passing a boolean flag.

// By default options mixed in :

wretch()
  .options({ headers: { "Accept": "application/json" }})
  .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }})

/*
{
  headers: { "Accept": "application/json", "X-Custom": "Header" },
  encoding: "same-origin"
}
*/

// With the flag, options are overridden :

wretch()
  .options({ headers: { "Accept": "application/json" }})
  .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, false)

/*
{
  headers: { "X-Custom": "Header" },
  encoding: "same-origin"
}
*/

headers(headerValues: Object)

Sets the request headers.

wretch("...")
  .headers({ "Content-Type": "text/plain", Accept: "application/json" })
  .post("my text")
  .json()

accept(headerValue: string)

Shortcut to set the "Accept" header.

wretch("...").accept("application/json")

content(headerValue: string)

Shortcut to set the "Content-Type" header.

wretch("...").content("application/json")

auth(headerValue: string)

Shortcut to set the "Authorization" header.

wretch("...").auth("Basic d3JldGNoOnJvY2tz")

catcher(errorId: number | string, catcher: (error: WretcherError, originalRequest: Wretcher) => void)

Adds a catcher which will be called on every subsequent request error.

Very useful when you need to perform a repetitive action on a specific error code.

const w = wretch()
  .catcher(404, err => redirect("/routes/notfound", err.message))
  .catcher(500, err => flashMessage("internal.server.error"))

// No need to catch the 404 or 500 codes, they are already taken care of.
w.url("http://myapi.com/get/something").get().json(json => /* ... */)

// Default catchers can be overridden if needed.
w
  .url("http://myapi.com/get/something")
  .get()
  .notFound(err => /* overrides the default 'redirect' catcher */)
  .json(json => /* ... */)

The original request is passed along the error and can be used in order to perform an additional request.

const reAuthOn401 = wretch()
  .catcher(401, async (error, request) => {
    // Renew credentials
    const token = await wretch("/renewtoken").get().text()
    storeToken(token)
    // Replay the original request with new credentials
    return request.auth(token).replay().unauthorized(err => { throw err }).json()
  })

reAuthOn401.url("/resource")
  .get()
  .json() // <- Will only be called for the original promise
  .then(callback) // <- Will be called for the original OR the replayed promise result

defer(callback: (originalRequest: Wretcher, url: string, options: Object) => Wretcher, clear = false)

Defer wretcher methods that will be chained and called just before the request is performed.

/* Small fictional example: deferred authentication */

// If you cannot retrieve the auth token while configuring the wretch object you can use .defer to postpone the call
const api = wretch("...").defer((w, url, options)  => {
  // If we are hitting the route /user…
  if(/\/user/.test(url)) {
    const { token } = options.context
    return w.auth(token)
  }
  return w
})

// ... //

const token = await getToken(request.session.user)

// .auth gets called here automatically
api.options({
  context: { token }
}).get().res()

resolve(doResolve: (chain: ResponseChain, originalRequest: Wretcher) => ResponseChain | Promise, clear = false)

Programs a resolver which will automatically be injected to perform response chain tasks.

Very useful when you need to perform repetitive actions on the wretch response.

The clear argument, if set to true, removes previously defined resolvers.

// Program "response" chain actions early on
const w = wretch()
  .resolve(resolver => resolver
    .perfs(_ =>  /* monitor every request */)
    .json(_ => _ /* automatically parse and return json */))

const myJson = await w.url("http://a.com").get()
// Equivalent to wretch()
//  .url("http://a.com")
//  .get()
//     <- the resolver chain is automatically injected here !
//  .perfs(_ =>  /* ... */)
//  .json(_ => _)

defaults(opts: Object, mixin: boolean = false)

Sets default fetch options which will be used for every subsequent requests.

// Interestingly enough, default options are mixed in :

wretch().defaults({ headers: { "Accept": "application/json" }})

// The fetch request is sent with both headers.
wretch("...", { headers: { "X-Custom": "Header" }}).get()
// You can mix in with the existing options instead of overriding them by passing a boolean flag :

wretch().defaults({ headers: { "Accept": "application/json" }})
wretch().defaults({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, true)

/* The new options are :
{
  headers: { "Accept": "application/json", "X-Custom": "Header" },
  encoding: "same-origin"
}
*/

errorType(method: "text" | "json" = "text")

Sets the method (text, json ...) used to parse the data contained in the response body in case of an HTTP error.

Persists for every subsequent requests.

wretch().errorType("json")

wretch("http://server/which/returns/an/error/with/a/json/body")
  .get()
  .res()
  .catch(error => {
    // error[errorType] (here, json) contains the parsed body
    console.log(error.json))
  }

polyfills(polyfills: Object)

Sets the non-global polyfills which will be used for every subsequent calls.

const fetch = require("node-fetch")
const FormData = require("form-data")

wretch().polyfills({
    fetch: fetch,
    FormData: FormData,
    URLSearchParams: require("url").URLSearchParams
})

Body Types

A body type is only needed when performing put/patch/post requests with a body.

body json formData formUrl

body(contents: any)

Sets the request body with any content.

wretch("...").body("hello").put()
// Note that calling an 'http verb' method with the body as an argument is equivalent:
wretch("...").put("hello")

json(jsObject: Object)

Sets the "Content-Type" header, stringifies an object and sets the request body.

const jsonObject = { a: 1, b: 2, c: 3 }
wretch("...").json(jsonObject).post()
// Note that calling an 'http verb' method with the object body as an argument is equivalent:
wretch("...").post(jsonObject)

formData(formObject: Object, recursive: string[] | boolean = false)

Converts the javascript object to a FormData and sets the request body.

const form = {
  hello: "world",
  duck: "Muscovy"
}
wretch("...").formData(form).post()

The recursive argument when set to true will enable recursion through all nested objects and produce object[key] keys. It can be set to an array of string to exclude specific keys.

Warning: Be careful to exclude Blob instances in the Browser, and ReadableStream and Buffer instances when using the node.js compatible form-data package.

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("...").formData(form, ["ignored"]).post()

formUrl(input: Object | string)

Converts the input parameter to an url encoded string and sets the content-type header and body. If the input argument is already a string, skips the conversion part.

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("...").formUrl(form).post()
wretch("...").formUrl(alreadyEncodedForm).post()

Http Methods

Required

You can pass optional fetch options and body arguments to these methods as a shorthand.

// This shorthand:
wretch().post({ json: 'body' }, { credentials: "same-origin" })
// Is equivalent to:
wretch().json({ json: 'body'}).options({ credentials: "same-origin" }).post()

NOTE: For methods having a body argument if the value is an Object it is assumed that it is a JSON payload and apply the same behaviour as calling .json(body), unless the Content-Type header has been set to something else beforehand.

get delete put patch post head opts

get(options)

Performs a get request.

wretch("...").get()

delete(options)

Performs a delete request.

wretch("...").delete()

put(body, options)

Performs a put request.

wretch("...").json({...}).put()

patch(body, options)

Performs a patch request.

wretch("...").json({...}).patch()

post(body, options)

Performs a post request.

wretch("...").json({...}).post()

head(options)

Performs a head request.

wretch("...").head()

opts(options)

Performs an options request.

wretch("...").opts()

Catchers

Catchers are optional, but if you do not provide them an error will still be thrown in case of an http error code received.

Catchers can be chained.

badRequest unauthorized forbidden notFound timeout internalError error fetchError
type WretcherError = Error & { status: number, response: WretcherResponse, text?: string, json?: Object }
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()

badRequest(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(400, cb).

unauthorized(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(401, cb).

forbidden(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(403, cb).

notFound(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(404, cb).

timeout(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(408, cb).

internalError(cb: (error: WretcherError, originalRequest: Wretcher) => any)

Syntactic sugar for error(500, cb).

error(errorId: number | string, cb: (error: WretcherError, originalRequest: Wretcher) => any)

Catches a specific error given its code or name and perform the callback.

fetchError(cb: (error: NetworkError, originalRequest: Wretcher) => any)

Catches any error thrown by the fetch function and perform the callback.


The original request is passed along the error and can be used in order to perform an additional request.

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

Required

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.

// 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(() => "Hello world!").then(console.log) // Hello world!

If an error is caught by catchers, the response type handler will not be called.

res json blob formData arrayBuffer text

res(cb?: (response : Response) => T) : Promise<Response | T>

Raw Response handler.

wretch("...").get().res(response => console.log(response.url))

json(cb?: (json : Object) => T) : Promise<Object | T>

Json handler.

wretch("...").get().json(json => console.log(Object.keys(json)))

blob(cb?: (blob : Blob) => T) : Promise<Blob | T>

Blob handler.

wretch("...").get().blob(blob => /* ... */)

formData(cb: (fd : FormData) => T) : Promise<FormData | T>

FormData handler.

wretch("...").get().formData(formData => /* ... */)

arrayBuffer(cb: (ab : ArrayBuffer) => T) : Promise<ArrayBuffer | T>

ArrayBuffer handler.

wretch("...").get().arrayBuffer(arrayBuffer => /* ... */)

text(cb: (text : string) => T) : Promise<string | T>

Text handler.

wretch("...").get().text(txt => console.log(txt))

Extras

A set of extra features.

Abortable requests Performance API Middlewares

Abortable requests

Only compatible with browsers that support AbortControllers. Otherwise, you could use a (partial) polyfill.

Use case :

const [c, w] = wretch("...")
  .get()
  .onAbort(_ => console.log("Aborted !"))
  .controller()

w.text(_ => console.log("should never be called"))
c.abort()

// Or :

const controller = new AbortController()

wretch("...")
  .signal(controller)
  .get()
  .onAbort(_ => console.log("Aborted !"))
  .text(_ => console.log("should never be called"))

controller.abort()

signal(controller: AbortController)

Used at "request time", like an helper.

Associates a custom controller with the request. Useful when you need to use your own AbortController, otherwise wretch will create a new controller itself.

const controller = new AbortController()

// Associates the same controller with multiple requests

wretch("url1")
  .signal(controller)
  .get()
  .json(_ => /* ... */)
wretch("url2")
  .signal(controller)
  .get()
  .json(_ => /* ... */)

// Aborts both requests

controller.abort()

setTimeout(time: number, controller?: AbortController)

Used at "response time".

Aborts the request after a fixed time. If you use a custom AbortController associated with the request, pass it as the second argument.

// 1 second timeout
wretch("...").get().setTimeout(1000).json(_ => /* will not be called in case of a timeout */)

controller()

Used at "response time".

Returns the automatically generated AbortController alongside the current wretch response as a pair.

// We need the controller outside the chain
const [c, w] = wretch("url")
  .get()
  .controller()

// Resume with the chain
w.onAbort(_ => console.log("ouch")).json(_ => /* ... */)

/* Later on ... */
c.abort()

onAbort(cb: (error: AbortError) => any)

Used at "response time" like a catcher.

Catches an AbortError and performs the callback.

Performance API

perfs(cb: (timings: PerformanceTiming) => void)

Takes advantage of the Performance API (browsers & node.js) to expose timings related to the underlying request.

Browser timings are very accurate, node.js only contains raw measures.

// Use perfs() before the response types (text, json, ...)
wretch("...")
  .get()
  .perfs(timings => {
    /* Will be called when the timings are ready. */
    console.log(timings.startTime)
  })
  .res()
  /* ... */

For node.js, there is a little extra work to do :

// Node.js 8.5+ only
const { performance, PerformanceObserver } = require("perf_hooks")

wretch().polyfills({
  fetch: function(url, opts) {
    performance.mark(url + " - begin")
    return fetch(url, opts).then(_ => {
      performance.mark(url + " - end")
      performance.measure(_.url, url + " - begin", url + " - end")
    })
  },
  /* other polyfills ... */
  performance: performance,
  PerformanceObserver: PerformanceObserver
})

Middlewares

Middlewares are functions that can intercept requests before being processed by Fetch. Wretch includes a helper to help replicate the middleware style.

Middlewares package

Check out wretch-middlewares, the official collection of middlewares.

Signature

Basically a Middleware is a function having the following signature :

// 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: WretcherOptions) => Promise<WretcherResponse>

middlewares(middlewares: ConfiguredMiddleware[], clear = false)

Add middlewares to intercept a request before being sent.

/* A simple delay middleware. */
const delayMiddleware = delay => next => (url, opts) => {
  return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
}

// The request will be delayed by 1 second.
wretch("...").middlewares([
  delayMiddleware(1000)
]).get().res(_ => /* ... */)

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.

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"

Middleware examples

/* 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)
}

License

MIT

Comments
  • Feature: recognize a `Wretch` error when catching request failure

    Feature: recognize a `Wretch` error when catching request failure

    Hi !

    Today when having a request error, an error object can be reached. cf. https://github.com/elbywan/wretch/blob/c624b20c194d039ad64a842d7033853380377d4f/src/resolver.ts#L74

    But if the request is embedded inside a ton of wrappers, when the error is catched, it is complicated to know if the error come from a wretch error or anything else.

    I just propose to create a custom Wretch error and make it exportable inside API, eg.

    export WretchError extends Error {
      constructor(msg, response) {
         super(msg);
         this.response = response;
      }
      get status() {
        return this.response.status;
      }
    }
    
    // inside in resolver.ts
    
     const err = new WretchError(msg, response);
     err[conf.errorType || "text"] = msg
    

    and in user code, we can do this:

    import { WretchError } from 'wretch.js';
    
    async function requestWithWretchAndOtherStuff() { [...] }
    
    try {
      const foo = await requestWithWretchAndOtherStuff()
    } except(err) {
      if (err instanceof WretchError) {
         // process request issue
      } else {
        // process other stuff not request related to request issue
      }
    }
    

    What do you think ?

    enhancement 
    opened by SamyCookie 13
  • Original request method

    Original request method

    So I'd like to create an app-wide wretch instance which will retry requests on unauthorized statuses. It would be pretty easy with wretch, but unfortunately, I don't really know how do I replay the same specific request.

    Because actually sending the request requires a call to .get(), .post() or whatever method it was, I can't replay the same request having a global instance.

    Am I missing something? Is there a way to send the request without calling the .[httpMethod]() function?

    enhancement question 
    opened by sarneeh 11
  • Test on multiple versions of node and clarify node support policy

    Test on multiple versions of node and clarify node support policy

    • [ ] πŸ˜› Didn't (Can't) test the CI run until opening this PR. Please don't merge until the CI pass
    • [x] 🧐 I think (but am not sure) browser test will emit coverage. So I've removed that part in browser CI
    • [x] πŸ€” Changing the supported node version is kinda breaking change? Should I use πŸ”₯ emoji in commit log instead?

    I am planning on writing tests that use native fetch from node >= 18. But want to land this CI change first to make sure my change won't break running test on earlier versions of node.

    opened by Holi0317 10
  • How to use your library as esm module?

    How to use your library as esm module?

    I'm trying to use through ttsc & typescript-transform-paths with paths in tsconfig.json

    {
    "wretch": ["https://cdn.jsdelivr.net/npm/[email protected]/dist/index.umd.js"]
    }
    

    but if I resolve wretch, any other related from index.umd.js are not resolved, cause of typescript-transform-paths resolved imports only in local app code, not node_modules =(

    Do you plan to publish esm version?

    enhancement 
    opened by viT-1 10
  • Issue when accessing endpoint that redirects to oauth provider

    Issue when accessing endpoint that redirects to oauth provider

    Currently, I am trying to call the backend endpoint that would redirect me to oauth provider using the following code

    wretch(`http://localhost:8080/auth/google`)
          .options({ mode: 'no-cors', credentials: 'include', redirect: 'follow' })
          .get();
    

    However, once the backend responds with 200, I get the following error in console.log

    throwingPromise resolver.js:41
        promise callback*resolver/throwingPromise< resolver.js:39
        promise callback*resolver resolver.js:36
        method wretcher.js:217
        get wretcher.js:223
    

    Any ideas on how to potentially debug this issue? I tried to follow the error trace but the error does not seem to be informative

    question 
    opened by semenodp 8
  • Problem sending blobs

    Problem sending blobs

    This might be more of a question than a bug. I'm trying to send a blob using wretch and the it looks like only {} is being sent.

    Example:

    let blob = new Blob()
    wretch('https://reqres.in/api/users/2').options({
      mode: 'cors'
    }).put(blob)
    

    Am I doing something wrong here?

    bug question 
    opened by onel 8
  • Allow to use function to get the base URL

    Allow to use function to get the base URL

    Some time I would like to get the base URL from some remote sources like remote config in FE and internal service registry in BE services. I found it relly painful in all apiClients(not only wretch) that you have to wrap it in function to make it dynamicly

    const getApiClient = ()=> {
      const url = getRemoteConfig() // get from remote sources
      return wretch(url)
    }
    
    // or
    
    // my current solution, I don't think it looks good, because it uses middleware in an unintended way, making the code hard to understand
    const apiClient = wretch().middlewares([
     next => (url, opts)=>{
      const base = getRemoteConfig() // get from remote sources
       return next(`${base}${url}`,opts)
      }
    ])
    
    

    I think it would be nice for the base URL param to support function, to separate it form middlewares

    const apiClient = wretch(()=>getRemoteConfig()).middlewares([
      // it can focus on other things
    ])
    
    
    question 
    opened by marklai1998 7
  • Feature: option to disable default errorType()

    Feature: option to disable default errorType()

    First of all, thank you for making Wretch. That's a very useful Fetch wrapper.

    When an error Response is returned by fetch(), Wretch always consumes it through a call to text() or json() depending on the configuration of errorType(). However, it can be useful some times to get the unconsumed raw response and let the error catcher handle it.

    Currently, it is not possible to disable the consuming of the reponse. Maybe a call to errorType() without parameter could be used for that. WDYT?

    question 
    opened by gbcreation 7
  • Using wretch in deno

    Using wretch in deno

    Hi there, I have been using wretch for a very long time, and hoping to use it in deno too.

    Since wretch is packaged as a esmodule, it is possible to import wretch directly in deno.

    import wretch from "https://cdn.pika.dev/wretch";

    However, I'm getting some typescript errors in some d.ts files in dist:

    error TS2304: Cannot find name 'AbortController'.
    
    β–Ί https://cdn.pika.dev/-/[email protected]/dist=es2019,mode=types/dist/resolver.d.ts:24:45
    
    24     setTimeout: (time: number, controller?: AbortController) => ResponseChain;
                                                   ~~~~~~~~~~~~~~~
    
    error TS2304: Cannot find name 'RequestInit'.
    
    β–Ί https://cdn.pika.dev/-/[email protected]/dist=es2019,mode=types/dist/wretcher.d.ts:4:39
    
    4 export declare type WretcherOptions = RequestInit & {
                                            ~~~~~~~~~~~
    
    error TS2304: Cannot find name 'AbortController'.
    
    β–Ί https://cdn.pika.dev/-/[email protected]/dist=es2019,mode=types/dist/wretcher.d.ts:109:24
    
    109     signal(controller: AbortController): Wretcher;
                               ~~~~~~~~~~~~~~~
    
    
    Found 3 errors.
    

    I can see they are not referenced anywhere in their corresponding files.

    Any idea what is causing these errors?

    Thank you for this amazing package!

    question 
    opened by shian15810 7
  • How to throw custom errors using the error() handler without trigger the final catch.

    How to throw custom errors using the error() handler without trigger the final catch.

    Hi, I would like to throw some custom errors on some specific http response codes (eg: 422 here), but I also need to throw a more generic error for all other status codes. So I tried something like this:

    // a very simplified version of my client API layer
    wretch("anything")
      .errorType('json')  
      .post(somedata)
      .error(422, error => {throw new ServerValidationError(error)}
      .res(response => /* ... */)
      .catch(error => {
          throw HttpError(error)
       })
    

    But throwing a 422 error reject the promise and trigger the catch callback at the end (that is the usual behavior on a promise based lib), but then, I got a HttpError instead of a ServerValidationError when I .catch() error on top of my api service.

    I was inspired by this example in the documentations:

    wretch("anything")
      .get()
      .notFound(error => { /* ... */ })
      .unauthorized(error => { /* ... */ })
      .error(418, error => { /* ... */ })
      .res(response => /* ... */)
      .catch(error => { /* uncaught errors */ })
    

    For me, this example means

    If response status code is 418, perform the attached callback, but do not fire the catch(). catch() is only for errors that are not handled by the error() or named errors handlers

    But it's probably a misunderstanding of this example, so I changed my code to something like this.

    wretch("anything")
      .errorType('json')  
      .post(somedata)
      .res(response => /* ... */)
      .catch(error => {
        if (error.response) {
          switch (error.response.status) {
            case 422:
              throw new ServerValidationError(error)
              break
            default:
              throw new HttpError(error)
              break
         }
        } else {
          throw new NetworkError(error)
        }
      })
    

    This works fine but i'm not sure it is the best way to achieve my goal, is there a better way to do this ?

    Thanks

    question 
    opened by MarlBurroW 7
  • handling error before .[response type]()

    handling error before .[response type]()

    First of all, awesome API!

    this works wretch().get().error() but not this wretch().error().get() gives .error is not a function.

    How can I setup a default error handler, before calling get/post/etc?

    question 
    opened by neves 7
Releases(2.3.0)
Owner
Julien Elbaz
Clever monkey. πŸ’ Available for hire. Previously @sonos @snipsco
Julien Elbaz
A tiny (304B to 489B) utility to check for deep equality

dequal A tiny (304B to 489B) utility to check for deep equality This module supports comparison of all types, including Function, RegExp, Date, Set, M

Luke Edwards 1.1k Dec 14, 2022
Complete Open Source Front End Candy Machine V2 Minter dAPP Built For The Frog Nation NFT Solana Project. Built With React, Candy Machine V2, Typescript

Complete Open Source Front End Candy Machine V2 Minter dAPP Built For The Frog Nation NFT Solana Project. Built With React, Candy Machine V2, Typescript

null 17 Sep 24, 2022
Solana blockchain candy machine app boilerplate on top of Metaplex Candy Machine. NextJS, Tailwind, Anchor, SolanaLabs.React, dev/mainnet automation scripts.

NFT Candy Factory NOTE: This repo will prob only work on unix-based environments. The NFT Candy Factory project is designed to let users fork, customi

Kevin Faveri 261 Dec 30, 2022
An even simpler wrapper around native Fetch to strip boilerplate from your fetching code!

Super lightweight (~450 bytes) wrapper to simplify native fetch calls using any HTTP method (existing or imagined). Features Fully typed/TypeScript su

Kevin R. Whitley 49 Dec 28, 2022
A tiny wrapper around pg that makes PostgreSQL a lot of fun to use. Written in TypeScript.

A tiny wrapper around pg that makes PostgreSQL a lot of fun to use. Written in TypeScript.

Mojolicious 8 Nov 29, 2022
spotify.ts is an wrapper built around Spotify's Web API

spotify.ts About spotify.ts is an wrapper built around Spotify's Web API. Features Fast Object Oriented Typescript, ESM, CJS support Easy to Use Insta

null 6 Nov 17, 2022
Modern Fetch API wrapper for simplicity.

FarFetch Class Modern Fetch API wrapper for simplicity. Install npm i @websitebeaver/far-fetch Then include it in the files you want to use it in lik

Website Beaver 53 Oct 28, 2022
🌳 Tiny & elegant JavaScript HTTP client based on the browser Fetch API

Huge thanks to for sponsoring me! Ky is a tiny and elegant HTTP client based on the browser Fetch API Ky targets modern browsers and Deno. For older b

Sindre Sorhus 8.5k Jan 2, 2023
✏️ Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped

Sugar High Introduction Super lightweight JSX syntax highlighter, around 1KB after minified and gzipped Usage npm install --save sugar-high import { h

Jiachi Liu 67 Dec 8, 2022
This repo contains instructions on how to create your NFT in Solana(using Metaplex and Candy Machine) and mint it using your custom front-end Dapp

Solana-NFT minting Dapp Create your own NFT's on Solana, and mint them from your custom front-end Dapp. Tools used Metaplex -> Metaplex is the NFT sta

Udit Sankhadasariya 12 Nov 2, 2022
Fork, customize and deploy your Candy Machine v2 super quickly

Candy Machine V2 Frontend This is a barebones implementation of Candy Machine V2 frontend, intended for users who want to quickly get started selling

AL 107 Oct 24, 2022
Candy Shop is a JavaScript library that allows DAOs, NFT projects and anyone to create an NFT marketplace on Solana in minutes!

Candy Shop (IN BETA) Intro Candy Shop is a JavaScript library that allows DAOs, NFT projects and anyone to create an NFT marketplace on Solana in minu

LIQNFT 111 Dec 15, 2022
Get any Candy Machine ID in seconds with this npm module!

What Does it do? Grabs Candy Machine ID of any v1 or v2 candy machine websites. Installation npm i candymachinescraper --save Example Usage // Get Can

Oscar Gomez 9 Oct 26, 2022
Wonka JS is the easiest way to mint Metaplex's Candy Machine NFTs with APIs.

Wonka JS Wonka JS is the easiest way to mint from Candy Machine and fetch NFTs through JS APIs. You can see an end to end example in Next.js demo proj

Wonka Labs 71 Nov 3, 2022
CandyPay SDK lets you effortlessly create NFT minting functions for Candy Machine v2 collections.

@candypay/sdk CandyPay SDK lets you effortlessly create NFT minting functions for Candy Machine v2 collections. Simulate minting transactions for mult

Candy Pay 33 Nov 16, 2022
React friendly API wrapper around MapboxGL JS

react-map-gl | Docs react-map-gl is a suite of React components designed to provide a React API for Mapbox GL JS-compatible libraries. More informatio

Vis.gl 6.9k Dec 31, 2022
React friendly API wrapper around MapboxGL JS

react-map-gl | Docs react-map-gl is a suite of React components designed to provide a React API for Mapbox GL JS-compatible libraries. More informatio

Vis.gl 6.9k Jan 2, 2023
Thin wrapper around Rant-Lang for Obsidian.md

Obsidian Rant-Lang Thin wrapper around the Rant language Rust crate to be used in Obsidian. "Rant is a high-level procedural templating language with

Leander Neiss 10 Jul 12, 2022
A thin wrapper around arweave-js for versioned permaweb document management.

?? ar-wrapper A thin wrapper around arweave-js for versioned permaweb document management. Helps to abstract away complexity for document storage for

verses 8 May 12, 2022
A simple nodejs module which is wrapper around solc that allows you to compile Solidity code

Simple Solidity Compiler It's a simple nodejs module which is wrapper around solc that allows you to compile Solidity code and get the abi and bytecod

King Rayhan 4 Feb 21, 2022