1 | <h1 align="center">
|
2 | <a href="https://elbywan.github.io/wretch"><img src="https://cdn.rawgit.com/elbywan/wretch/08831345/wretch.svg" alt="wretch-logo" width="220px"></a><br>
|
3 | <br>
|
4 | <a href="https://elbywan.github.io/wretch">Wretch</a><br>
|
5 | <br>
|
6 | <a href="https://elbywan.github.io/wretch"><img alt="homepage-badge" src="https://img.shields.io/website-up-down-green-red/http/shields.io.svg?label=wretch-homepage"></a>
|
7 | <a href="https://travis-ci.org/elbywan/wretch"><img alt="travis-badge" src="https://travis-ci.org/elbywan/wretch.svg?branch=master"></a>
|
8 | <a href="https://www.npmjs.com/package/wretch"><img alt="npm-badge" src="https://img.shields.io/npm/v/wretch.svg?colorB=ff733e" height="20"></a>
|
9 | <a href="https://www.npmjs.com/package/wretch"><img alt="npm-downloads-badge" src="https://img.shields.io/npm/dm/wretch.svg?colorB=53aabb" height="20"></a>
|
10 | <a href="https://coveralls.io/github/elbywan/wretch?branch=master"><img src="https://coveralls.io/repos/github/elbywan/wretch/badge.svg?branch=master" alt="Coverage Status"></a>
|
11 | <a href="https://github.com/elbywan/wretch/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license-badge" height="20"></a>
|
12 | </h1>
|
13 | <h4 align="center">
|
14 | A tiny (< 2.5Kb g-zipped) wrapper built around fetch with an intuitive syntax.
|
15 | </h4>
|
16 | <h5 align="center">
|
17 | <i>f[ETCH] [WR]apper</i>
|
18 | </h6>
|
19 |
|
20 | <br>
|
21 |
|
22 | ##### Wretch 1.2 is now live 🎉 ! Please check out 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, look at the [dev](https://github.com/elbywan/wretch/tree/dev) branch.
|
23 |
|
24 | ##### A collection of middlewares is available through the [wretch-middlewares](https://github.com/elbywan/wretch-middlewares) package! 📦
|
25 |
|
26 | # Table of Contents
|
27 |
|
28 | * [Motivation](#motivation)
|
29 | * [Installation](#installation)
|
30 | * [Compatibility](#compatibility)
|
31 | * [Usage](#usage)
|
32 | * [Api](#api)
|
33 | * [License](#license)
|
34 |
|
35 | # Motivation
|
36 |
|
37 | #### Because having to write a second callback to process a response body feels awkward.
|
38 |
|
39 | ```javascript
|
40 | // Fetch needs a second callback to process the response body
|
41 |
|
42 | fetch("examples/example.json")
|
43 | .then(response => response.json())
|
44 | .then(json => {
|
45 | //Do stuff with the parsed json
|
46 | })
|
47 | ```
|
48 |
|
49 | ```javascript
|
50 | // Wretch does it for you
|
51 |
|
52 | // Use .res for the raw response, .text for raw text, .json for json, .blob for a blob ...
|
53 | wretch("examples/example.json")
|
54 | .get()
|
55 | .json(json => {
|
56 | // Do stuff with the parsed json
|
57 | })
|
58 | ```
|
59 |
|
60 | #### Because manually checking and throwing every request error code is fastidious.
|
61 |
|
62 | ```javascript
|
63 | // Fetch won’t reject on HTTP error status
|
64 |
|
65 | fetch("anything")
|
66 | .then(response => {
|
67 | if(!response.ok) {
|
68 | if(response.status === 404) throw new Error("Not found")
|
69 | else if(response.status === 401) throw new Error("Unauthorized")
|
70 | else if(response.status === 418) throw new Error("I'm a teapot !")
|
71 | else throw new Error("Other error")
|
72 | }
|
73 | else // ...
|
74 | })
|
75 | .then(data => /* ... */)
|
76 | .catch(error => { /* ... */ })
|
77 | ```
|
78 |
|
79 | ```javascript
|
80 | // Wretch throws when the response is not successful and contains helper methods to handle common codes
|
81 |
|
82 | wretch("anything")
|
83 | .get()
|
84 | .notFound(error => { /* ... */ })
|
85 | .unauthorized(error => { /* ... */ })
|
86 | .error(418, error => { /* ... */ })
|
87 | .res(response => /* ... */)
|
88 | .catch(error => { /* uncaught errors */ })
|
89 | ```
|
90 |
|
91 | #### Because sending a json object should be easy.
|
92 |
|
93 | ```javascript
|
94 | // With fetch you have to set the header, the method and the body manually
|
95 |
|
96 | fetch("endpoint", {
|
97 | method: "POST",
|
98 | headers: { "Content-Type": "application/json" },
|
99 | body: JSON.stringify({ "hello": "world" })
|
100 | }).then(response => /* ... */)
|
101 | // Omitting the data retrieval and error management parts
|
102 | ```
|
103 |
|
104 | ```javascript
|
105 | // With wretch, you have shorthands at your disposal
|
106 |
|
107 | wretch("endpoint")
|
108 | .post({ "hello": "world" })
|
109 | .res(response => /* ... */)
|
110 | ```
|
111 |
|
112 | #### Because configuration should not rhyme with repetition.
|
113 |
|
114 | ```javascript
|
115 | // Wretch object is immutable which means that you can configure, store and reuse instances
|
116 |
|
117 | // Cross origin authenticated requests on an external API
|
118 | const externalApi = wretch()
|
119 | // Set the base url
|
120 | .url("http://external.api")
|
121 | // Authorization header
|
122 | .auth(`Bearer ${ token }`)
|
123 | // Cors fetch options
|
124 | .options({ credentials: "include", mode: "cors" })
|
125 | // Handle 403 errors
|
126 | .resolve(_ => _.forbidden(handle403))
|
127 |
|
128 | // Fetch a resource
|
129 | externalApi
|
130 | .url("/resource/1")
|
131 | // Add a custom header for this request
|
132 | .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
|
133 | .get()
|
134 | .json(handleResource)
|
135 | // Post a resource
|
136 | externalApi
|
137 | .url("/resource")
|
138 | .post({ "Shiny new": "resource object" })
|
139 | .json(handleNewResourceResult)
|
140 | ```
|
141 |
|
142 | # Installation
|
143 |
|
144 | ## Npm
|
145 |
|
146 | ```sh
|
147 | npm i wretch
|
148 | ```
|
149 |
|
150 | ## Clone
|
151 |
|
152 | ```sh
|
153 | git clone https://github.com/elbywan/wretch
|
154 | cd wretch
|
155 | npm install
|
156 | npm start
|
157 | ```
|
158 |
|
159 | # Compatibility
|
160 |
|
161 | ## Browsers
|
162 |
|
163 | Wretch is compatible with modern browsers out of the box.
|
164 |
|
165 | For older environments without fetch support, you should get a [polyfill](https://github.com/github/fetch).
|
166 |
|
167 | ## Node.js
|
168 |
|
169 | Works with any [FormData](https://github.com/form-data/form-data) or [fetch](https://www.npmjs.com/package/node-fetch) polyfills.
|
170 |
|
171 | ```javascript
|
172 | // The global way :
|
173 |
|
174 | global.fetch = require("node-fetch")
|
175 | global.FormData = require("form-data")
|
176 | global.URLSearchParams = require("url").URLSearchParams
|
177 |
|
178 | // Or the non-global way :
|
179 |
|
180 | wretch().polyfills({
|
181 | fetch: require("node-fetch"),
|
182 | FormData: require("form-data"),
|
183 | URLSearchParams: require("url").URLSearchParams
|
184 | })
|
185 | ```
|
186 |
|
187 | # Usage
|
188 |
|
189 | **Wretch is bundled using the UMD format (@`dist/bundle/wretch.js`) alongside es2015 modules (@`dist/index.js`) and typescript definitions.**
|
190 |
|
191 | ## Import
|
192 |
|
193 | ```html
|
194 | <!--- "wretch" will be attached to the global window object. -->
|
195 | <script src="https://unpkg.com/wretch"></script>
|
196 | ```
|
197 |
|
198 | ```typescript
|
199 | // es2015 modules
|
200 | import wretch from "wretch"
|
201 |
|
202 | // commonjs
|
203 | var wretch = require("wretch")
|
204 | ```
|
205 |
|
206 | ## Code
|
207 |
|
208 | **Wretcher objects are immutable.**
|
209 |
|
210 | ```javascript
|
211 | wretch(url, options)
|
212 |
|
213 | /* The "request" chain. */
|
214 |
|
215 | .[helper method(s)]()
|
216 | // [ Optional ]
|
217 | // A set of helper methods to set the default options, set accept header, change the current url ...
|
218 | .[body type]()
|
219 | // [ Optional ]
|
220 | // Serialize an object to json or FormData formats and sets the body & header field if needed
|
221 | .[http method]()
|
222 | // [ Required, ends the request chain ]
|
223 | // Performs the get/put/post/delete/patch request
|
224 |
|
225 | /* Fetch is called at this time. */
|
226 | /* The request is sent, and from this point on you can chain catchers and call a response type handler. */
|
227 |
|
228 | /* The "response" chain. */
|
229 |
|
230 | .[catcher(s)]()
|
231 | // [ Optional ]
|
232 | // You can chain error handlers here
|
233 | .[response type]()
|
234 | // [ Required, ends the response chain ]
|
235 | // Specify the data type you need, which will be parsed and handed to you
|
236 |
|
237 | /* From this point wretch returns a standard Promise, so you can continue chaining actions afterwards. */
|
238 |
|
239 | .then(/* ... */)
|
240 | .catch(/* ... */)
|
241 | ```
|
242 |
|
243 | # API
|
244 |
|
245 | * [Helper Methods](#helper-methods)
|
246 | * [Body Types](#body-types)
|
247 | * [Http Methods](#http-methods)
|
248 | * [Catchers](#catchers)
|
249 | * [Response Types](#response-types)
|
250 | * [Extras](#extras)
|
251 |
|
252 | ------
|
253 |
|
254 | #### wretch(url = "", opts = {})
|
255 |
|
256 | Creates a new Wretcher object with an url and [vanilla fetch options](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch).
|
257 |
|
258 | ## Helper Methods
|
259 |
|
260 | *Helper methods are optional and can be chained.*
|
261 |
|
262 | | [url](#urlurl-string-replace-boolean--false) | [query](#queryqp-object--string-replace-boolean) | [options](#optionsoptions-object-mixin-boolean--true) | [headers](#headersheadervalues-object) | [accept](#acceptheadervalue-string) | [content](#contentheadervalue-string) | [auth](#authheadervalue-string) | [catcher](#catchererrorid-number--string-catcher-error-wretchererror-originalrequest-wretcher--void) | [resolve](#resolvedoresolve-chain-responsechain-originalrequest-wretcher--responsechain--promise-clear--false) | [defaults](#defaultsopts-object-mixin-boolean--false) | [errorType](#errortypemethod-text--json--text) | [polyfills](#polyfillspolyfills-object) |
|
263 | |-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
|
264 |
|
265 | #### url(url: string, replace: boolean = false)
|
266 |
|
267 | Appends or replaces the url.
|
268 |
|
269 | ```js
|
270 | wretch().url("...").get().json(/* ... */)
|
271 |
|
272 | // Can be used to set a base url
|
273 |
|
274 | // Subsequent requests made using the 'blogs' object will be prefixed with "http://mywebsite.org/api/blogs"
|
275 | const blogs = wretch("http://mywebsite.org/api/blogs")
|
276 |
|
277 | // Perfect for CRUD apis
|
278 | const id = await blogs.post({ name: "my blog" }).json(_ => _.id)
|
279 | const blog = await blogs.url(`/${id}`).get().json()
|
280 | console.log(blog.name)
|
281 |
|
282 | await blogs.url(`/${id}`).delete().res()
|
283 |
|
284 | // And to replace the base url if needed :
|
285 | const noMoreBlogs = blogs.url("http://mywebsite.org/", true)
|
286 | ```
|
287 |
|
288 | #### query(qp: object | string, replace: boolean)
|
289 |
|
290 | Converts a javascript object to query parameters, then appends this query string to the current url.
|
291 | String values are used as the query string verbatim.
|
292 |
|
293 | Pass `true` as the second argument to replace existing query parameters.
|
294 |
|
295 | ```js
|
296 | let w = wretch("http://example.com")
|
297 | // url is http://example.com
|
298 | w = w.query({ a: 1, b: 2 })
|
299 | // url is now http://example.com?a=1&b=2
|
300 | w = w.query({ c: 3, d: [4, 5] })
|
301 | // url is now http://example.com?a=1&b=2c=3&d=4&d=5
|
302 | w = w.query("five&six&seven=eight")
|
303 | // url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight
|
304 | w = w.query({ reset: true }, true)
|
305 | // url is now http://example.com?reset=true
|
306 | ```
|
307 |
|
308 | #### options(options: Object, mixin: boolean = true)
|
309 |
|
310 | Sets the fetch options.
|
311 |
|
312 | ```js
|
313 | wretch("...").options({ credentials: "same-origin" })
|
314 | ```
|
315 |
|
316 | Wretch being immutable, you can store the object for later use.
|
317 |
|
318 | ```js
|
319 | const corsWretch = wretch().options({ credentials: "include", mode: "cors" })
|
320 |
|
321 | corsWretch.url("http://endpoint1").get()
|
322 | corsWretch.url("http://endpoint2").get()
|
323 | ```
|
324 |
|
325 | You can override instead of mixing in the existing options by passing a boolean flag.
|
326 |
|
327 | ```js
|
328 | // By default options mixed in :
|
329 |
|
330 | wretch()
|
331 | .options({ headers: { "Accept": "application/json" }})
|
332 | .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }})
|
333 |
|
334 | /*
|
335 | {
|
336 | headers: { "Accept": "application/json", "X-Custom": "Header" },
|
337 | encoding: "same-origin"
|
338 | }
|
339 | */
|
340 |
|
341 | // With the flag, options are overridden :
|
342 |
|
343 | wretch()
|
344 | .options({ headers: { "Accept": "application/json" }})
|
345 | .options({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, false)
|
346 |
|
347 | /*
|
348 | {
|
349 | headers: { "X-Custom": "Header" },
|
350 | encoding: "same-origin"
|
351 | }
|
352 | */
|
353 | ```
|
354 |
|
355 | #### headers(headerValues: Object)
|
356 |
|
357 | Sets the request headers.
|
358 |
|
359 | ```js
|
360 | wretch("...")
|
361 | .headers({ "Content-Type": "text/plain", Accept: "application/json" })
|
362 | .post("my text")
|
363 | .json()
|
364 | ```
|
365 |
|
366 | #### accept(headerValue: string)
|
367 |
|
368 | Shortcut to set the "Accept" header.
|
369 |
|
370 | ```js
|
371 | wretch("...").accept("application/json")
|
372 | ```
|
373 |
|
374 | #### content(headerValue: string)
|
375 |
|
376 | Shortcut to set the "Content-Type" header.
|
377 |
|
378 | ```js
|
379 | wretch("...").content("application/json")
|
380 | ```
|
381 |
|
382 | #### auth(headerValue: string)
|
383 |
|
384 | Shortcut to set the "Authorization" header.
|
385 |
|
386 | ```js
|
387 | wretch("...").auth("Basic d3JldGNoOnJvY2tz")
|
388 | ```
|
389 |
|
390 | #### catcher(errorId: number | string, catcher: (error: WretcherError, originalRequest: Wretcher) => void)
|
391 |
|
392 | Adds a [catcher](https://github.com/elbywan/wretch#catchers) which will be called on every subsequent request error.
|
393 |
|
394 | Very useful when you need to perform a repetitive action on a specific error code.
|
395 |
|
396 | ```js
|
397 | const w = wretch()
|
398 | .catcher(404, err => redirect("/routes/notfound", err.message))
|
399 | .catcher(500, err => flashMessage("internal.server.error"))
|
400 | .error("SyntaxError", err => log("bad.json"))
|
401 |
|
402 | // No need to catch 404 or 500 code or the json parsing error, they are already taken care of.
|
403 | w.url("http://myapi.com/get/something").get().json(json => /* ... */)
|
404 |
|
405 | // Default catchers can be overridden if needed.
|
406 | w.url("...").notFound(err => /* overrides the default 'redirect' catcher */)
|
407 | ```
|
408 |
|
409 | The original request is passed along the error and can be used in order to perform an additional request.
|
410 |
|
411 | ```js
|
412 | const reAuthOn401 = wretch()
|
413 | .catcher(401, async (error, request) => {
|
414 | // Renew credentials
|
415 | const token = await wretch("/renewtoken").get().text()
|
416 | storeToken(token)
|
417 | // Replay the original request with new credentials
|
418 | return request.auth(token).get().unauthorized(err => { throw err }).json()
|
419 | })
|
420 |
|
421 | reAuthOn401.url("/resource")
|
422 | .get()
|
423 | .json() // <- Will only be called for the original promise
|
424 | .then(callback) // <- Will be called for the original OR the replayed promise result
|
425 | ```
|
426 |
|
427 | #### defer(callback: (originalRequest: Wretcher, url: string, options: Object) => Wretcher, clear = false)
|
428 |
|
429 | Defer wretcher methods that will be chained and called just before the request is performed.
|
430 |
|
431 | ```js
|
432 | /* Small fictional example: deferred authentication */
|
433 |
|
434 | // If you cannot retrieve the auth token while configuring the wretch object you can use .defer to postpone the call
|
435 | const api = wretch("...").defer((w, url, options) => {
|
436 | // If we are hitting the route /user…
|
437 | if(/\/user/.test(url)) {
|
438 | const { token } = options.context
|
439 | return w.auth(token)
|
440 | }
|
441 | return w
|
442 | })
|
443 |
|
444 | // ... //
|
445 |
|
446 | const token = await getToken(request.session.user)
|
447 |
|
448 | // .auth gets called here automatically
|
449 | api.options({
|
450 | context: { token }
|
451 | }).get().res()
|
452 | ```
|
453 |
|
454 | #### resolve(doResolve: (chain: ResponseChain, originalRequest: Wretcher) => ResponseChain | Promise<any>, clear = false)
|
455 |
|
456 | Programs a resolver which will automatically be injected to perform response chain tasks.
|
457 |
|
458 | Very useful when you need to perform repetitive actions on the wretch response.
|
459 |
|
460 | *The clear argument, if set to true, removes previously defined resolvers.*
|
461 |
|
462 | ```js
|
463 | // Program "response" chain actions early on
|
464 | const w = wretch()
|
465 | .resolve(resolver => resolver
|
466 | .perfs(_ => /* monitor every request */)
|
467 | .json(_ => _ /* automatically parse and return json */))
|
468 |
|
469 | const myJson = await w.url("http://a.com").get()
|
470 | // Equivalent to wretch()
|
471 | // .url("http://a.com")
|
472 | // .get()
|
473 | // <- the resolver chain is automatically injected here !
|
474 | // .perfs(_ => /* ... */)
|
475 | // .json(_ => _)
|
476 | ```
|
477 |
|
478 | #### defaults(opts: Object, mixin: boolean = false)
|
479 |
|
480 | Sets default fetch options which will be used for every subsequent requests.
|
481 |
|
482 | ```js
|
483 | // Interestingly enough, default options are mixed in :
|
484 |
|
485 | wretch().defaults({ headers: { "Accept": "application/json" }})
|
486 |
|
487 | // The fetch request is sent with both headers.
|
488 | wretch("...", { headers: { "X-Custom": "Header" }}).get()
|
489 | ```
|
490 |
|
491 | ```js
|
492 | // You can mix in with the existing options instead of overriding them by passing a boolean flag :
|
493 |
|
494 | wretch().defaults({ headers: { "Accept": "application/json" }})
|
495 | wretch().defaults({ encoding: "same-origin", headers: { "X-Custom": "Header" }}, true)
|
496 |
|
497 | /* The new options are :
|
498 | {
|
499 | headers: { "Accept": "application/json", "X-Custom": "Header" },
|
500 | encoding: "same-origin"
|
501 | }
|
502 | */
|
503 | ```
|
504 |
|
505 | #### errorType(method: "text" | "json" = "text")
|
506 |
|
507 | Sets the method (text, json ...) used to parse the data contained in the response body in case of an HTTP error.
|
508 |
|
509 | Persists for every subsequent requests.
|
510 |
|
511 | ```js
|
512 | wretch().errorType("json")
|
513 |
|
514 | wretch("http://server/which/returns/an/error/with/a/json/body")
|
515 | .get()
|
516 | .res()
|
517 | .catch(error => {
|
518 | // error[errorType] (here, json) contains the parsed body
|
519 | console.log(error.json))
|
520 | }
|
521 | ```
|
522 |
|
523 | #### polyfills(polyfills: Object)
|
524 |
|
525 | Sets the non-global polyfills which will be used for every subsequent calls.
|
526 |
|
527 | ```javascript
|
528 | const fetch = require("node-fetch")
|
529 | const FormData = require("form-data")
|
530 |
|
531 | wretch().polyfills({
|
532 | fetch: fetch,
|
533 | FormData: FormData,
|
534 | URLSearchParams: require("url").URLSearchParams
|
535 | })
|
536 | ```
|
537 |
|
538 | ## Body Types
|
539 |
|
540 | *A body type is only needed when performing put/patch/post requests with a body.*
|
541 |
|
542 | | [body](#bodycontents-any) | [json](#jsonjsobject-object) | [formData](#formdataformobject-object) | [formUrl](#formurlinput--object--string) |
|
543 | |-----|-----|-----|-----|
|
544 |
|
545 | #### body(contents: any)
|
546 |
|
547 | Sets the request body with any content.
|
548 |
|
549 | ```js
|
550 | wretch("...").body("hello").put()
|
551 | // Note that calling an 'http verb' method with the body as an argument is equivalent:
|
552 | wretch("...").put("hello")
|
553 | ```
|
554 |
|
555 | #### json(jsObject: Object)
|
556 |
|
557 | Sets the content type header, stringifies an object and sets the request body.
|
558 |
|
559 | ```js
|
560 | const jsonObject = { a: 1, b: 2, c: 3 }
|
561 | wretch("...").json(jsonObject).post()
|
562 | // Note that calling an 'http verb' method with the object body as an argument is equivalent:
|
563 | wretch("...").post(jsonObject)
|
564 |
|
565 | ```
|
566 |
|
567 | #### formData(formObject: Object)
|
568 |
|
569 | Converts the javascript object to a FormData and sets the request body.
|
570 |
|
571 | ```js
|
572 | const form = {
|
573 | hello: "world",
|
574 | duck: "Muscovy"
|
575 | }
|
576 | wretch("...").formData(form).post()
|
577 | ```
|
578 |
|
579 | #### formUrl(input: Object | string)
|
580 |
|
581 | Converts the input parameter to an url encoded string and sets the content-type header and body.
|
582 | If the input argument is already a string, skips the conversion part.
|
583 |
|
584 | ```js
|
585 | const form = { a: 1, b: { c: 2 }}
|
586 | const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D"
|
587 |
|
588 | // Automatically sets the content-type header to "application/x-www-form-urlencoded"
|
589 | wretch("...").formUrl(form).post()
|
590 | wretch("...").formUrl(alreadyEncodedForm).post()
|
591 | ```
|
592 |
|
593 | ## Http Methods
|
594 |
|
595 | **Required**
|
596 |
|
597 | *You can pass optional fetch options and body arguments to these methods as a shorthand.*
|
598 |
|
599 | ```js
|
600 | // This shorthand:
|
601 | wretch().post({ json: 'body' }, { credentials: "same-origin" })
|
602 | // Is equivalent to:
|
603 | wretch().json({ json: 'body'}).options({ credentials: "same-origin" }).post()
|
604 | ```
|
605 |
|
606 | | [get](#getopts) | [delete](#deleteopts) | [put](#putbody-opts) | [patch](#patchbody-opts) | [post](#postbody-opts) | [head](#headopts) | [opts](#optsopts) |
|
607 | |-----|-----|-----|-----|-----|-----|-----|
|
608 |
|
609 | #### get(options)
|
610 |
|
611 | Performs a get request.
|
612 |
|
613 | ```js
|
614 | wretch("...").get()
|
615 | ```
|
616 |
|
617 | #### delete(options)
|
618 |
|
619 | Performs a delete request.
|
620 |
|
621 | ```js
|
622 | wretch("...").delete()
|
623 | ```
|
624 |
|
625 | #### put(body, options)
|
626 |
|
627 | Performs a put request.
|
628 |
|
629 | ```js
|
630 | wretch("...").json({...}).put()
|
631 | ```
|
632 |
|
633 | #### patch(body, options)
|
634 |
|
635 | Performs a patch request.
|
636 |
|
637 | ```js
|
638 | wretch("...").json({...}).patch()
|
639 | ```
|
640 |
|
641 | #### post(body, options)
|
642 |
|
643 | Performs a post request.
|
644 |
|
645 | ```js
|
646 | wretch("...").json({...}).post()
|
647 | ```
|
648 |
|
649 | #### head(options)
|
650 |
|
651 | Performs a head request.
|
652 |
|
653 | ```js
|
654 | wretch("...").head()
|
655 | ```
|
656 | #### opts(options)
|
657 |
|
658 | Performs an options request.
|
659 |
|
660 | ```js
|
661 | wretch("...").opts()
|
662 | ```
|
663 |
|
664 | ## Catchers
|
665 |
|
666 | *Catchers are optional, but if you do not provide them an error will still be thrown in case of an http error code received.*
|
667 |
|
668 | *Catchers can be chained.*
|
669 |
|
670 | | [badRequest](#badrequestcb-error-wretchererror-originalrequest-wretcher--any) | [unauthorized](#unauthorizedcb-error-wretchererror-originalrequest-wretcher--any) | [forbidden](#forbiddencb-error-wretchererror-originalrequest-wretcher--any) | [notFound](#notfoundcb-error-wretchererror-originalrequest-wretcher--any) | [timeout](#timeoutcb-error-wretchererror-originalrequest-wretcher--any) | [internalError](#internalerrorcb-error-wretchererror-originalrequest-wretcher--any) | [error](#errorerrorid-number--string-cb-error-wretchererror-originalrequest-wretcher--any) |
|
671 | |-----|-----|-----|-----|-----|-----|-----|
|
672 |
|
673 | ```ts
|
674 | type WretcherError = Error & { status: number, response: WretcherResponse, text?: string, json?: Object }
|
675 | ```
|
676 |
|
677 | ```js
|
678 | wretch("...")
|
679 | .get()
|
680 | .badRequest(err => console.log(err.status))
|
681 | .unauthorized(err => console.log(err.status))
|
682 | .forbidden(err => console.log(err.status))
|
683 | .notFound(err => console.log(err.status))
|
684 | .timeout(err => console.log(err.status))
|
685 | .internalError(err => console.log(err.status))
|
686 | .error(418, err => console.log(err.status))
|
687 | .res()
|
688 | ```
|
689 |
|
690 | #### badRequest(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
691 |
|
692 | Syntactic sugar for `error(400, cb)`.
|
693 |
|
694 | #### unauthorized(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
695 |
|
696 | Syntactic sugar for `error(401, cb)`.
|
697 |
|
698 | #### forbidden(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
699 |
|
700 | Syntactic sugar for `error(403, cb)`.
|
701 |
|
702 | #### notFound(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
703 |
|
704 | Syntactic sugar for `error(404, cb)`.
|
705 |
|
706 | #### timeout(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
707 |
|
708 | Syntactic sugar for `error(418, cb)`.
|
709 |
|
710 | #### internalError(cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
711 |
|
712 | Syntactic sugar for `error(500, cb)`.
|
713 |
|
714 | #### error(errorId: number | string, cb: (error: WretcherError, originalRequest: Wretcher) => any)
|
715 |
|
716 | Catches a specific error given its code or name and perform the callback.
|
717 |
|
718 | ---------
|
719 |
|
720 | The original request is passed along the error and can be used in order to perform an additional request.
|
721 |
|
722 | ```js
|
723 | wretch("/resource")
|
724 | .get()
|
725 | .unauthorized(async (error, req) => {
|
726 | // Renew credentials
|
727 | const token = await wretch("/renewtoken").get().text()
|
728 | storeToken(token)
|
729 | // Replay the original request with new credentials
|
730 | return req.auth(token).get().unauthorized(err => { throw err }).json()
|
731 | })
|
732 | .json()
|
733 | // The promise chain is preserved as expected
|
734 | // ".then" will be performed on the result of the original request
|
735 | // or the replayed one (if a 401 error was thrown)
|
736 | .then(callback)
|
737 | ```
|
738 |
|
739 | ## Response Types
|
740 |
|
741 | **Required**
|
742 |
|
743 | *If an error is caught by catchers, the response type handler will not be called.*
|
744 |
|
745 | | [res](#rescb-response--response--any) | [json](#jsoncb-json--object--any) | [blob](#blobcb-blob--blob--any) | [formData](#formdatacb-fd--formdata--any) | [arrayBuffer](#arraybuffercb-ab--arraybuffer--any) | [text](#textcb-text--string--any) |
|
746 | |-----|-----|-----|-----|-----|-----|
|
747 |
|
748 | #### res(cb: (response : Response) => any)
|
749 |
|
750 | Raw Response handler.
|
751 |
|
752 | ```js
|
753 | wretch("...").get().res(response => console.log(response.url))
|
754 | ```
|
755 |
|
756 | #### json(cb: (json : Object) => any)
|
757 |
|
758 | Json handler.
|
759 |
|
760 | ```js
|
761 | wretch("...").get().json(json => console.log(Object.keys(json)))
|
762 | ```
|
763 |
|
764 | #### blob(cb: (blob : Blob) => any)
|
765 |
|
766 | Blob handler.
|
767 |
|
768 | ```js
|
769 | wretch("...").get().blob(blob => /* ... */)
|
770 | ```
|
771 |
|
772 | #### formData(cb: (fd : FormData) => any)
|
773 |
|
774 | FormData handler.
|
775 |
|
776 | ```js
|
777 | wretch("...").get().formData(formData => /* ... */)
|
778 | ```
|
779 |
|
780 | #### arrayBuffer(cb: (ab : ArrayBuffer) => any)
|
781 |
|
782 | ArrayBuffer handler.
|
783 |
|
784 | ```js
|
785 | wretch("...").get().arrayBuffer(arrayBuffer => /* ... */)
|
786 | ```
|
787 |
|
788 | #### text(cb: (text : string) => any)
|
789 |
|
790 | Text handler.
|
791 |
|
792 | ```js
|
793 | wretch("...").get().text(txt => console.log(txt))
|
794 | ```
|
795 |
|
796 | ## Extras
|
797 |
|
798 | *A set of extra features.*
|
799 |
|
800 | | [Abortable requests](#abortable-requests) | [Performance API](#performance-api) | [Middlewares](#middlewares) |
|
801 | |-----|-----|-----|
|
802 |
|
803 | ### Abortable requests
|
804 |
|
805 | *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).*
|
806 |
|
807 | Use case :
|
808 |
|
809 | ```js
|
810 | const [c, w] = wretch("...")
|
811 | .get()
|
812 | .onAbort(_ => console.log("Aborted !"))
|
813 | .controller()
|
814 |
|
815 | w.text(_ => console.log("should never be called"))
|
816 | c.abort()
|
817 |
|
818 | // Or :
|
819 |
|
820 | const controller = new AbortController()
|
821 |
|
822 | wretch("...")
|
823 | .signal(controller)
|
824 | .get()
|
825 | .onAbort(_ => console.log("Aborted !"))
|
826 | .text(_ => console.log("should never be called"))
|
827 |
|
828 | c.abort()
|
829 | ```
|
830 |
|
831 | ### signal(controller: AbortController)
|
832 |
|
833 | *Used at "request time", like an helper.*
|
834 |
|
835 | Associates a custom controller with the request.
|
836 | Useful when you need to use your own AbortController, otherwise wretch will create a new controller itself.
|
837 |
|
838 | ```js
|
839 | const controller = new AbortController()
|
840 |
|
841 | // Associates the same controller with multiple requests
|
842 |
|
843 | wretch("url1")
|
844 | .signal(controller)
|
845 | .get()
|
846 | .json(_ => /* ... */)
|
847 | wretch("url2")
|
848 | .signal(controller)
|
849 | .get()
|
850 | .json(_ => /* ... */)
|
851 |
|
852 | // Aborts both requests
|
853 |
|
854 | controller.abort()
|
855 | ```
|
856 |
|
857 | #### setTimeout(time: number, controller?: AbortController)
|
858 |
|
859 | *Used at "response time".*
|
860 |
|
861 | Aborts the request after a fixed time. If you use a custom AbortController associated with the request, pass it as the second argument.
|
862 |
|
863 | ```js
|
864 | // 1 second timeout
|
865 | wretch("...").get().setTimeout(1000).json(_ => /* will not be called in case of a timeout */)
|
866 | ```
|
867 |
|
868 | #### controller()
|
869 |
|
870 | *Used at "response time".*
|
871 |
|
872 | Returns the automatically generated AbortController alongside the current wretch response as a pair.
|
873 |
|
874 | ```js
|
875 | // We need the controller outside the chain
|
876 | const [c, w] = wretch("url")
|
877 | .get()
|
878 | .controller()
|
879 |
|
880 | // Resume with the chain
|
881 | w.onAbort(_ => console.log("ouch")).json(_ => /* ... */)
|
882 |
|
883 | /* Later on ... */
|
884 | c.abort()
|
885 | ```
|
886 |
|
887 | #### onAbort(cb: (error: AbortError) => any)
|
888 |
|
889 | *Used at "response time" like a catcher.*
|
890 |
|
891 | Catches an AbortError and performs the callback.
|
892 |
|
893 | ### Performance API
|
894 |
|
895 | #### perfs(cb: (timings: PerformanceTiming) => void)
|
896 |
|
897 | Takes advantage of the Performance API ([browsers](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) & [node.js](https://nodejs.org/api/perf_hooks.html)) to expose timings related to the underlying request.
|
898 |
|
899 | Browser timings are very accurate, node.js only contains raw measures.
|
900 |
|
901 | ```js
|
902 | // Use perfs() before the response types (text, json, ...)
|
903 | wretch("...")
|
904 | .get()
|
905 | .perfs(timings => {
|
906 | /* Will be called when the timings are ready. */
|
907 | console.log(timings.startTime)
|
908 | })
|
909 | .res()
|
910 | /* ... */
|
911 | ```
|
912 |
|
913 | For node.js, there is a little extra work to do :
|
914 |
|
915 | ```js
|
916 | // Node.js 8.5+ only
|
917 | const { performance, PerformanceObserver } = require("perf_hooks")
|
918 |
|
919 | wretch().polyfills({
|
920 | fetch: function(url, opts) {
|
921 | performance.mark(url + " - begin")
|
922 | return fetch(url, opts).then(_ => {
|
923 | performance.mark(url + " - end")
|
924 | performance.measure(_.url, url + " - begin", url + " - end")
|
925 | })
|
926 | },
|
927 | /* other polyfills ... */
|
928 | performance: performance,
|
929 | PerformanceObserver: PerformanceObserver
|
930 | })
|
931 | ```
|
932 |
|
933 | ### Middlewares
|
934 |
|
935 | Middlewares are functions that can intercept requests before being processed by Fetch.
|
936 | Wretch includes a helper to help replicate the [middleware](http://expressjs.com/en/guide/using-middleware.html) style.
|
937 |
|
938 |
|
939 | #### Middlewares package
|
940 |
|
941 | Check out [wretch-middlewares](https://github.com/elbywan/wretch-middlewares), the official collection of middlewares.
|
942 |
|
943 | #### Signature
|
944 |
|
945 | Basically a Middleware is a function having the following signature :
|
946 |
|
947 | ```ts
|
948 | // A middleware accepts options and returns a configured version
|
949 | type Middleware = (options?: {[key: string]: any}) => ConfiguredMiddleware
|
950 | // A configured middleware (with options curried)
|
951 | type ConfiguredMiddleware = (next: FetchLike) => FetchLike
|
952 | // A "fetch like" function, accepting an url and fetch options and returning a response promise
|
953 | type FetchLike = (url: string, opts: WretcherOptions) => Promise<WretcherResponse>
|
954 | ```
|
955 |
|
956 | #### middlewares(middlewares: ConfiguredMiddleware[], clear = false)
|
957 |
|
958 | Add middlewares to intercept a request before being sent.
|
959 |
|
960 | ```javascript
|
961 | /* A simple delay middleware. */
|
962 | const delayMiddleware = delay => next => (url, opts) => {
|
963 | return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
|
964 | }
|
965 |
|
966 | // The request will be delayed by 1 second.
|
967 | wretch("...").middlewares([
|
968 | delayMiddleware(1000)
|
969 | ]).get().res(_ => /* ... */)
|
970 | ```
|
971 |
|
972 | #### Middleware examples
|
973 |
|
974 | ```javascript
|
975 | /* A simple delay middleware. */
|
976 | const delayMiddleware = delay => next => (url, opts) => {
|
977 | return new Promise(res => setTimeout(() => res(next(url, opts)), delay))
|
978 | }
|
979 |
|
980 | /* Returns the url and method without performing an actual request. */
|
981 | const shortCircuitMiddleware = () => next => (url, opts) => {
|
982 | // We create a new Response object to comply because wretch expects that from fetch.
|
983 | const response = new Response()
|
984 | response.text = () => Promise.resolve(opts.method + "@" + url)
|
985 | response.json = () => Promise.resolve({ url, method: opts.method })
|
986 | // Instead of calling next(), returning a Response Promise bypasses the rest of the chain.
|
987 | return Promise.resolve(response)
|
988 | }
|
989 |
|
990 | /* Logs all requests passing through. */
|
991 | const logMiddleware = () => next => (url, opts) => {
|
992 | console.log(opts.method + "@" + url)
|
993 | return next(url, opts)
|
994 | }
|
995 |
|
996 | /* A throttling cache. */
|
997 | const cacheMiddleware = (throttle = 0) => {
|
998 |
|
999 | const cache = new Map()
|
1000 | const inflight = new Map()
|
1001 | const throttling = new Set()
|
1002 |
|
1003 | return next => (url, opts) => {
|
1004 | const key = opts.method + "@" + url
|
1005 |
|
1006 | if(!opts.noCache && throttling.has(key)) {
|
1007 | // If the cache contains a previous response and we are throttling, serve it and bypass the chain.
|
1008 | if(cache.has(key))
|
1009 | return Promise.resolve(cache.get(key).clone())
|
1010 | // If the request in already in-flight, wait until it is resolved
|
1011 | else if(inflight.has(key)) {
|
1012 | return new Promise((resolve, reject) => {
|
1013 | inflight.get(key).push([resolve, reject])
|
1014 | })
|
1015 | }
|
1016 | }
|
1017 |
|
1018 | // Init. the pending promises Map
|
1019 | if(!inflight.has(key))
|
1020 | inflight.set(key, [])
|
1021 |
|
1022 | // If we are not throttling, activate the throttle for X milliseconds
|
1023 | if(throttle && !throttling.has(key)) {
|
1024 | throttling.add(key)
|
1025 | setTimeout(() => { throttling.delete(key) }, throttle)
|
1026 | }
|
1027 |
|
1028 | // We call the next middleware in the chain.
|
1029 | return next(url, opts)
|
1030 | .then(_ => {
|
1031 | // Add a cloned response to the cache
|
1032 | cache.set(key, _.clone())
|
1033 | // Resolve pending promises
|
1034 | inflight.get(key).forEach((([resolve, reject]) => resolve(_.clone()))
|
1035 | // Remove the inflight pending promises
|
1036 | inflight.delete(key)
|
1037 | // Return the original response
|
1038 | return _
|
1039 | })
|
1040 | .catch(_ => {
|
1041 | // Reject pending promises on error
|
1042 | inflight.get(key).forEach(([resolve, reject]) => reject(_))
|
1043 | inflight.delete(key)
|
1044 | throw _
|
1045 | })
|
1046 | }
|
1047 | }
|
1048 |
|
1049 | // To call a single middleware
|
1050 | const cache = cacheMiddleware(1000)
|
1051 | wretch("...").middlewares([cache]).get()
|
1052 |
|
1053 | // To chain middlewares
|
1054 | wretch("...").middlewares([
|
1055 | logMiddleware(),
|
1056 | delayMiddleware(1000),
|
1057 | shortCircuitMiddleware()
|
1058 | }).get().text(_ => console.log(text))
|
1059 |
|
1060 | // To test the cache middleware more thoroughly
|
1061 | const wretchCache = wretch().middlewares([cacheMiddleware(1000)])
|
1062 | const printResource = (url, timeout = 0) =>
|
1063 | setTimeout(_ => wretchCache.url(url).get().notFound(console.error).text(console.log), timeout)
|
1064 | // The resource url, change it to an invalid route to check the error handling
|
1065 | const resourceUrl = "/"
|
1066 | // Only two actual requests are made here even though there are 30 calls
|
1067 | for(let i = 0; i < 10; i++) {
|
1068 | printResource(resourceUrl)
|
1069 | printResource(resourceUrl, 500)
|
1070 | printResource(resourceUrl, 1500)
|
1071 | }
|
1072 | ```
|
1073 |
|
1074 | # License
|
1075 |
|
1076 | MIT
|