-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
866 lines (807 loc) · 30.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
//. # Fluture Node
//.
//. FP-style HTTP and streaming utils for Node based on [Fluture][].
//.
//. Skip to the [Http section](#http) for the main code example.
//.
//. ## Usage
//.
//. ```console
//. $ npm install --save fluture fluture-node
//. ```
//.
//. On Node 12 and up, this module can be loaded directly with `import` or
//. `require`. On Node versions below 12, `require` or the [esm][]-loader can
//. be used.
//.
//. ## API
import http from 'http';
import https from 'https';
import qs from 'querystring';
import {Readable, pipeline} from 'stream';
import {isDeepStrictEqual} from 'util';
import {lookup} from 'dns';
import {
Future,
attempt,
chain,
encase,
map,
mapRej,
pap,
reject,
resolve,
} from 'fluture/index.js';
const hasProp = k => o => Object.prototype.hasOwnProperty.call (o, k);
//. ### EventEmitter
//# once :: String -> EventEmitter -> Future Error a
//.
//. Resolve a Future with the first event emitted over
//. the given event emitter under the given event name.
//.
//. When the Future is cancelled, it removes any trace of
//. itself from the event emitter.
//.
//. ```js
//. > const emitter = new EventEmitter ();
//. > setTimeout (() => emitter.emit ('answer', 42), 100);
//. > once ('answer') (emitter);
//. Future.of (42);
//. ```
export const once = event => emitter => Future ((rej, res) => {
const removeListeners = () => {
emitter.removeListener ('error', onError);
emitter.removeListener (event, onEvent);
};
const onError = x => {
removeListeners ();
rej (x);
};
const onEvent = x => {
removeListeners ();
res (x);
};
emitter.once ('error', onError);
emitter.once (event, onEvent);
return removeListeners;
});
//. ### Buffer
//# encode :: Charset -> Buffer -> Future Error String
//.
//. Given an encoding and a [Buffer][], returns a Future of the result of
//. encoding said buffer using the given encoding. The Future will reject
//. with an Error if the encoding is unknown.
//.
//. ```js
//. > encode ('utf8') (Buffer.from ('Hello world!'));
//. 'Hello world!'
//. ```
export const encode = encoding => buffer => (
mapRej (e => new Error (e.message))
(attempt (() => buffer.toString (encoding)))
);
//. ### Stream
//# streamOf :: Buffer -> Future a (Readable Buffer)
//.
//. Given a [Buffer][], returns a Future of a [Readable][] stream which will
//. emit the given Buffer before ending.
//.
//. The stream is wrapped in a Future because creation of a stream causes
//. side-effects if it's not consumed in time, making it safer to pass it
//. around wrapped in a Future.
export const streamOf = encase (buf => new Readable ({
highWaterMark: buf.byteLength,
read: function() {
if (this._pushed || this.push (buf)) { this.push (null); }
this._pushed = true;
},
}));
//# emptyStream :: Future a (Readable Buffer)
//.
//. A [Readable][] stream which ends after emiting zero bytes. Can be useful
//. as an empty [`Request`](#Request) body, for example.
export const emptyStream = streamOf (Buffer.alloc (0));
//# buffer :: Readable a -> Future Error (Array a)
//.
//. Buffer all data on a [Readable][] stream into a Future of an Array.
//.
//. When the Future is cancelled, it removes any trace of
//. itself from the Stream.
//.
//. ```js
//. > const stream = new Readable ({read: () => {}});
//. > setTimeout (() => {
//. . stream.push ('hello');
//. . stream.push ('world');
//. . stream.push (null);
//. . }, 100);
//. > buffer (stream);
//. Future.of ([Buffer.from ('hello'), Buffer.from ('world')]);
//. ```
export const buffer = stream => Future ((rej, res) => {
const chunks = [];
const removeListeners = () => {
stream.removeListener ('data', onData);
stream.removeListener ('error', rej);
stream.removeListener ('end', onEnd);
};
const onData = d => chunks.push (d);
const onEnd = () => {
removeListeners ();
res (chunks);
};
const onError = e => {
removeListeners ();
rej (e);
};
stream.on ('data', onData);
stream.once ('error', onError);
stream.once ('end', onEnd);
return removeListeners;
});
//# bufferString :: Charset -> Readable Buffer -> Future Error String
//.
//. A version of [`buffer`](#buffer) specialized in Strings.
//.
//. Takes a charset and a [Readable][] stream of [Buffer][]s, and returns
//. a Future containing a String with the fully buffered and encoded result.
export const bufferString = charset => stream => (
chain (encode (charset)) (map (Buffer.concat) (buffer (stream)))
);
//. ### Event Loop
//# instant :: b -> Future a b
//.
//. Resolves a Future with the given value in the next tick,
//. using [`process.nextTick`][]. The scheduled job cannot be
//. cancelled and will run before any other jobs, effectively
//. blocking the event loop until it's completed.
//.
//. ```js
//. > instant ('noodles')
//. Future.of ('noodles')
//. ```
export const instant = x => Future ((rej, res) => {
process.nextTick (res, x);
return () => {};
});
//# immediate :: b -> Future a b
//.
//. Resolves a Future with the given value in the next tick,
//. using [`setImmediate`][]. This job will run as soon as all
//. other jobs are completed. When the Future is cancelled, the
//. job is unscheduled.
//.
//. ```js
//. > immediate ('results')
//. Future.of ('results')
//. ```
export const immediate = x => Future ((rej, res) => {
const job = setImmediate (res, x);
return () => { clearImmediate (job); };
});
//. ### Http
//.
//. The functions below are to be used in compositions such as the one shown
//. below, in order to cover a wide variety of HTTP-related use cases.
//.
//. ```js
//. import {reject, map, chain, encase, fork} from 'fluture';
//. import {retrieve,
//. matchStatus,
//. followRedirects,
//. autoBufferResponse,
//. responseToError} from 'fluture-node';
//.
//. const json = res => (
//. chain (encase (JSON.parse)) (autoBufferResponse (res))
//. );
//.
//. const notFound = res => (
//. chain (({message}) => reject (new Error (message))) (json (res))
//. );
//.
//. retrieve ('https://api.github.com/users/Avaq') ({'User-Agent': 'Avaq'})
//. .pipe (chain (followRedirects (20)))
//. .pipe (chain (matchStatus (responseToError) ({200: json, 404: notFound})))
//. .pipe (map (avaq => avaq.name))
//. .pipe (fork (console.error) (console.log));
//. ```
//.
//. The example above will either:
//.
//. 1. log `"Aldwin Vlasblom"` to the terminal if nothing weird happens; or
//. 2. Report a 404 error using the message returned from the server; or
//. 3. log an error to the console if:
//. * a network error occurs;
//. * the response code is not what we expect; or
//. * the JSON is malformed.
//.
//. Note that we were in control of the following:
//.
//. - How redirects are followed: We use [`followRedirects`](#followRedirects)
//. with a maxmum of 20 redirects, but we could have used a different
//. redirection function using [`followRedirectsWith`](#followRedirectsWith)
//. with the [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy) or
//. even a fully custom policy.
//.
//. - How an unexpected status was treated: We passed in a handler to
//. [`matchStatus`](#matchStatus).
//. We used [`responseToError`](#responseToError), conviently provided by
//. this library, but we could have used a custom mechanism.
//.
//. - How responses with expected status codes are treated:
//. The [`matchStatus`](#matchStatus) function lets us provide a handler
//. based on the status code of the response. Each handler has full control
//. over the response.
//.
//. - How the response body is buffered and decoded: Our `json` function uses
//. [`autoBufferResponse`](#autoBufferResponse) to buffer and decode the
//. response according to the mime type provided in the headers. However, we
//. could have used lower level functions, such as
//. [`bufferResponse`](#bufferResponse) or even just [`buffer`](#buffer).
//.
//. - How the response body is parsed: We used [`Fluture.encase`][] with
//. [`JSON.parse`][] to parse JSON with a safe failure path. However, we
//. could have used a more refined approach to parsing the JSON, for
//. example by using [`S.parseJson`][].
//.
//. The goal is to give you as much control over HTTP requests and responses
//. as possible, while still keeping boilerplate low by leveraging function
//. composition.
//.
//. This contrasts with many of the popular HTTP client libraries out there,
//. which either make decisions for you, taking away control in an attempt to
//. provide a smoother usage experience, or which take complicated structures
//. of interacting options to attempt to cater to as many cases as possible.
// defaultCharset :: String
const defaultCharset = 'utf8';
// defaultContentType :: String
const defaultContentType = 'text/plain; charset=' + defaultCharset;
// charsetRegex :: RegExp
const charsetRegex = /\bcharset=([^;\s]+)/;
// mimeTypes :: StrMap Mimetype
const mimeTypes = {
form: 'application/x-www-form-urlencoded; charset=utf8',
json: 'application/json; charset=utf8',
};
// getRequestModule :: String -> Future Error Module
const getRequestModule = protocol => {
switch (protocol) {
case 'https:': return resolve (https);
case 'http:': return resolve (http);
default: return reject (new Error (`Unsupported protocol '${protocol}'`));
}
};
//# Request :: Object -> Url -> Future Error (Readable Buffer) -> Request
//.
//. Constructs a value of type Request to be used as an argument for
//. functions such as [`sendRequest`](#sendRequest).
//.
//. Takes the following arguments:
//.
//. 1. An Object containing any [http options][] except: `auth`, `host`,
//. `hostname`, `path`, `port`, and `protocol`; because they are part of
//. the URL, and `signal`; because Fluture handles the cancellation.
//. 2. A String containing the request URL.
//. 3. A Future of a [Readable][] stream of [Buffer][]s to be used as the
//. request body. Note that the Future must produce a brand new Stream
//. every time it is forked, or if it can't, it is expected to reject
//. with a value of type Error.
//.
//. See [`sendRequest`](#sendRequest) for a usage example.
export const Request = options => url => body => ({options, url, body});
//# Request.options :: Request -> Object
//.
//. Get the options out of a Request.
Request.options = ({options}) => options;
//# Request.url :: Request -> Url
//.
//. Get the url out of a Request.
Request.url = ({url}) => url;
//# Request.body :: Request -> Future Error (Readable Buffer)
//.
//. Get the body out of a Request.
Request.body = ({body}) => body;
//# Response :: Request -> IncomingMessage -> Response
//.
//. Constructs a value of type Response. These values are typically created
//. for you by functions such as [`sendRequest`](#sendRequest).
//. Takes the following arguments:
//.
//. 1. A [Request](#Request).
//. 2. An [IncomingMessage][] assumed to belong to the Request.
export const Response = request => message => ({request, message});
//# Response.request :: Response -> Request
//.
//. Get the request out of a Response.
Response.request = ({request}) => request;
//# Response.message :: Response -> IncomingMessage
//.
//. Get the message out of a Response.
Response.message = ({message}) => message;
// cleanRequestOptions :: Request -> Object
export const cleanRequestOptions = request => {
const options = Request.options (request);
return {
agent: options.agent,
createConnection: options.createConnection,
defaultPort: options.defaultPort || (
options.agent && options.agent.defaultPort
),
family: options.family,
headers: options.headers || {},
insecureHTTPParser: options.insecureHTTPParser === true,
localAddress: options.localAddress,
lookup: options.lookup || lookup,
maxHeaderSize: options.maxHeaderSize || 16384,
method: (options.method || 'GET').toUpperCase (),
setHost: options.setHost !== false,
socketPath: options.socketPath,
timeout: options.timeout,
};
};
//# sendRequest :: Request -> Future Error Response
//.
//. This is the "lowest level" function for making HTTP requests. It does not
//. handle buffering, encoding, content negotiation, or anything really.
//. For most use cases, you can use one of the more specialized functions:
//.
//. * [`send`](#send): Make a generic HTTP request.
//. * [`retrieve`](#retrieve): Make a GET request.
//.
//. Given a [Request](#Request), returns a Future which makes an HTTP request
//. and resolves with the resulting [Response](#Response).
//. If the Future is cancelled, the request is aborted.
//.
//. ```js
//. import {attempt} from 'fluture';
//. import {createReadStream} from 'fs';
//.
//. const BinaryPostRequest = Request ({
//. method: 'POST',
//. headers: {'Transfer-Encoding': 'chunked'},
//. });
//.
//. const eventualBody = attempt (() => createReadStream ('./data.bin'));
//.
//. sendRequest (BinaryPostRequest ('https://example.com') (eventualBody));
//. ```
//.
//. If you want to use this function to transfer a stream of data, don't forget
//. to set the Transfer-Encoding header to "chunked".
export const sendRequest = request => {
const location = new URL (Request.url (request));
const makeRequest = lib => stream => Future ((rej, res) => {
const req = lib.request (location, cleanRequestOptions (request));
const onResponse = response => res (Response (request) (response));
req.once ('response', onResponse);
pipeline (stream, req, e => e && rej (e));
return () => {
req.removeListener ('response', onResponse);
req.abort ();
};
});
return (
getRequestModule (location.protocol)
.pipe (map (makeRequest))
.pipe (pap (Request.body (request)))
.pipe (chain (x => x))
);
};
//# retrieve :: Url -> StrMap String -> Future Error Response
//.
//. A version of [`sendRequest`](#sendRequest) specialized in the `GET` method.
//.
//. Given a URL and a StrMap of request headers, returns a Future which
//. makes a GET requests to the given resource.
//.
//. ```js
//. retrieve ('https://api.github.com/users/Avaq') ({'User-Agent': 'Avaq'})
//. ```
export const retrieve = url => headers => (
sendRequest (Request ({headers}) (url) (emptyStream))
);
//# send :: Mimetype -> Method -> Url -> StrMap String -> Buffer -> Future Error Response
//.
//. A version of [`sendRequest`](#sendRequest) for sending arbitrary data to
//. a server. There's also more specific versions for sending common types of
//. data:
//.
//. * [`sendJson`](#sendJson) sends JSON stringified data.
//. * [`sendForm`](#sendForm) sends form encoded data.
//.
//. Given a MIME type, a request method, a URL, a StrMap of headers, and
//. finally a Buffer, returns a Future which will send the Buffer to the
//. server at the given URL using the given request method, telling it the
//. buffer contains data of the given MIME type.
//.
//. This function will always send the Content-Type and Content-Length headers,
//. alongside the provided headers. Manually provoding either of these headers
//. override those generated by this function.
export const send = mime => method => url => extraHeaders => buf => {
const headers = Object.assign ({
'Content-Type': mime,
'Content-Length': buf.byteLength,
}, extraHeaders);
return sendRequest (Request ({method, headers}) (url) (streamOf (buf)));
};
//# sendJson :: Method -> String -> StrMap String -> JsonValue -> Future Error Response
//.
//. A version of [`send`](#send) specialized in sending JSON.
//.
//. Given a request method, a URL, a StrMap of headers and a JavaScript plain
//. object, returns a Future which sends the object to the server at the
//. given URL after JSON-encoding it.
//.
//. ```js
//. sendJson ('PUT')
//. ('https://example.com/users/bob')
//. ({Authorization: 'Bearer asd123'})
//. ({name: 'Bob', email: '[email protected]'});
//. ```
export const sendJson = method => url => headers => json => {
const buf = Buffer.from (JSON.stringify (json));
return send (mimeTypes.json) (method) (url) (headers) (buf);
};
//# sendForm :: Method -> String -> StrMap String -> JsonValue -> Future Error Response
//.
//. A version of [`send`](#send) specialized in sending form data.
//.
//. Given a request method, a URL, a StrMap of headers and a JavaScript plain
//. object, returns a Future which sends the object to the server at the
//. given URL after www-form-urlencoding it.
//.
//. ```js
//. sendForm ('POST')
//. ('https://example.com/users/create')
//. ({})
//. ({name: 'Bob', email: '[email protected]'});
//. ```
export const sendForm = method => url => headers => form => {
const buf = Buffer.from (qs.stringify (form));
return send (mimeTypes.form) (method) (url) (headers) (buf);
};
//# matchStatus :: (Response -> a) -> StrMap (Response -> a) -> Response -> a
//.
//. Transform a [`Response`](#Response) based on its status code.
//.
//. ```js
//. import {chain} from 'fluture';
//.
//. const processResponse = matchStatus (responseToError) ({
//. 200: autoBufferResponse,
//. });
//.
//. chain (processResponse) (retreive ('https://example.com'));
//. ```
//.
//. This is kind of like a `switch` statement on the status code of the
//. Response message. Or, if you will, a pattern match against the
//. Response type if you imagine it being tagged via the status code.
//.
//. The first argument is the "default" case, and the second argument is a
//. map of status codes to functions that should have the same type as the
//. first argument.
//.
//. The resulting function `Response -> a` has the same signature as the input
//. functions, meaning you can use `matchStatus` *again* to "extend" the
//. pattern by passing the old pattern as the "default" case for the new one:
//.
//. ```js
//. import {reject} from 'fluture';
//.
//. matchStatus (processResponse) ({
//. 404: () => reject (new Error ('Example not found!')),
//. });
//. ```
export const matchStatus = f => fs => res => {
const {statusCode} = Response.message (res);
return (hasProp (statusCode) (fs) ? fs[statusCode] : f) (res);
};
// mergeUrls :: (Url, Any) -> String
const mergeUrls = (base, input) => (
typeof input === 'string' ?
new URL (input, base).href :
base
);
// sameOrigin :: (Url, Url) -> Boolean
const sameOrigin = (parent, child) => {
const p = new URL (parent);
const c = new URL (child);
return (p.protocol === c.protocol || c.protocol === 'https:') &&
(p.host === c.host || c.host.endsWith ('.' + p.host));
};
// overHeaders :: (Request, Array2 String String -> Array2 String String)
// -> Request
const overHeaders = (request, f) => {
const options = cleanRequestOptions (request);
const headers = Object.fromEntries (f (Object.entries (options.headers)));
return Request (Object.assign ({}, Request.options (request), {headers}))
(Request.url (request))
(Request.body (request));
};
// confidentialHeaders :: Array String
const confidentialHeaders = [
'authorization',
'proxy-authorization',
'cookie',
];
//# redirectAnyRequest :: Response -> Request
//.
//. A redirection strategy that simply reissues the original Request to the
//. Location specified in the given Response.
//.
//. If the new location is on an external host, then any confidential headers
//. (such as the cookie header) will be dropped from the new request.
//.
//. Used in the [`defaultRedirectionPolicy`](#defaultRedirectionPolicy) and
//. the [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy).
export const redirectAnyRequest = response => {
const {headers: {location}} = Response.message (response);
const original = Response.request (response);
const oldUrl = Request.url (original);
const newUrl = mergeUrls (oldUrl, location);
const request = Request (Request.options (original))
(newUrl)
(Request.body (original));
return sameOrigin (oldUrl, newUrl) ? request : overHeaders (request, xs => (
xs.filter (([name]) => !confidentialHeaders.includes (name.toLowerCase ()))
));
};
//# redirectIfGetMethod :: Response -> Request
//.
//. A redirection strategy that simply reissues the original Request to the
//. Location specified in the given Response, but only if the original request
//. was using the GET method.
//.
//. If the new location is on an external host, then any confidential headers
//. (such as the cookie header) will be dropped from the new request.
//.
//. Used in [`followRedirectsStrict`](#followRedirectsStrict).
export const redirectIfGetMethod = response => {
const {method} = cleanRequestOptions (Response.request (response));
return (
method === 'GET' ?
redirectAnyRequest (response) :
Response.request (response)
);
};
//# redirectUsingGetMethod :: Response -> Request
//.
//. A redirection strategy that sends a new GET request based on the original
//. request to the Location specified in the given Response. If the response
//. does not contain a valid location, the request is not redirected.
//.
//. The original request method and body are discarded, but other options
//. are preserved. If the new location is on an external host, then any
//. confidential headers (such as the cookie header) will be dropped from the
//. new request.
//.
//. Used in the [`defaultRedirectionPolicy`](#defaultRedirectionPolicy) and
//. the [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy).
export const redirectUsingGetMethod = response => {
const original = Response.request (response);
const options = Object.assign ({}, Request.options (original), {
method: 'GET',
});
const request = Request (options) (Request.url (original)) (emptyStream);
return redirectAnyRequest (Response (request) (Response.message (response)));
};
// See https://developer.mozilla.org/docs/Web/HTTP/Headers#Conditionals
const conditionHeaders = [
'if-match',
'if-modified-since',
'if-none-match',
'if-unmodified-since',
];
//# retryWithoutCondition :: Response -> Request
//.
//. A redirection strategy that removes any caching headers if present and
//. retries the request, or does nothing if no caching headers were present
//. on the original request.
//.
//. Used in the [`defaultRedirectionPolicy`](#defaultRedirectionPolicy).
export const retryWithoutCondition = response => {
const original = Response.request (response);
const {method} = cleanRequestOptions (original);
const request = overHeaders (original, xs => xs.filter (([name]) => (
!(conditionHeaders.includes (name.toLowerCase ()))
)));
return method === 'GET' ? request : original;
};
//# defaultRedirectionPolicy :: Response -> Request
//.
//. Carefully follows redirects in strict accordance with
//. [RFC2616 Section 10.3][].
//.
//. Redirections with status codes 301, 302, and 307 are only followed if the
//. original request used the GET method, and redirects with status code 304
//. are left alone for a caching layer to deal with.
//.
//. This redirection policy is used by default in the
//. [`followRedirects`](#followRedirects) function. You can extend it, using
//. [`matchStatus`](#matchStatus) to create a custom redirection policy, as
//. shown in the example:
//.
//. See also [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy).
//.
//. ```js
//. const redirectToBestOption = () => {
//. // Somehow figure out which URL to redirect to.
//. };
//.
//. const myRedirectionPolicy = matchStatus (defaultRedirectionPolicy) ({
//. 300: redirectToBestOption,
//. 301: redirectUsingGetMethod,
//. });
//.
//. retrieve ('https://example.com') ({})
//. .pipe (chain (followRedirectsWith (myRedirectionPolicy) (10)))
//. ```
export const defaultRedirectionPolicy = matchStatus (Response.request) ({
301: redirectIfGetMethod,
302: redirectIfGetMethod,
303: redirectUsingGetMethod,
305: redirectAnyRequest,
307: redirectIfGetMethod,
});
//# aggressiveRedirectionPolicy :: Response -> Request
//.
//. Aggressively follows redirects in mild violation of
//. [RFC2616 Section 10.3][]. In particular, anywhere that a redirection
//. should be interrupted for user confirmation or caching, this policy
//. follows the redirection nonetheless.
//.
//. Redirections with status codes 301, 302, and 307 are always followed
//. without user intervention, and redirects with status code 304 are
//. retried without conditions if the original request had any conditional
//. headers.
//.
//. See also [`defaultRedirectionPolicy`](defaultRedirectionPolicy).
//.
//. ```js
//. retrieve ('https://example.com') ({})
//. .pipe (chain (followRedirectsWith (aggressiveRedirectionPolicy) (10)))
//. ```
export const aggressiveRedirectionPolicy = matchStatus (Response.request) ({
301: redirectAnyRequest,
302: redirectAnyRequest,
303: redirectUsingGetMethod,
304: retryWithoutCondition,
305: redirectAnyRequest,
307: redirectAnyRequest,
});
// requestsEquivalent :: Request -> Request -> Boolean
const requestsEquivalent = left => right => (
isDeepStrictEqual (
cleanRequestOptions (left),
cleanRequestOptions (right)
) &&
Request.url (left) === Request.url (right) &&
Request.body (left) === Request.body (right)
);
//# followRedirectsWith :: (Response -> Request) -> Number -> Response -> Future Error Response
//.
//. Given a function that take a Response and produces a new Request, and a
//. "maximum" number, recursively keeps resolving new requests until a request
//. is encountered that was seen before, or the maximum number is reached.
//.
//. See [`followRedirects`](#followRedirects) for an out-of-the-box redirect-
//. follower. See [`aggressiveRedirectionPolicy`](#aggressiveRedirectionPolicy)
//. and [`defaultRedirectionPolicy`](defaultRedirectionPolicy) for
//. additional usage examples.
export const followRedirectsWith = strategy => _max => _response => {
const seen = [];
const followUp = max => response => {
if (max < 1) {
return resolve (response);
}
seen.push (Response.request (response));
const nextRequest = strategy (response);
for (let i = seen.length - 1; i >= 0; i -= 1) {
if (requestsEquivalent (seen[i]) (nextRequest)) {
return resolve (response);
}
}
return (
sendRequest (nextRequest)
.pipe (mapRej (e => new Error ('After redirect: ' + e.message)))
.pipe (chain (followUp (max - 1)))
);
};
return followUp (_max) (_response);
};
//# followRedirects :: Number -> Response -> Future Error Response
//.
//. Given the maximum numbers of redirections, follows redirects according to
//. the [default redirection policy](#defaultRedirectionPolicy).
//.
//. See the [Http section](#http) for a usage example.
export const followRedirects = followRedirectsWith (defaultRedirectionPolicy);
//# acceptStatus :: Number -> Response -> Future Response Response
//.
//. This function "tags" a [Response](#Response) based on a given status code.
//. If the response status matches the given status code, the returned Future
//. will resolve. If it doesn't, the returned Future will reject.
//.
//. See also [`matchStatus`](#matchStatus), which will probably be more useful
//. in most cases.
//.
//. The idea is that you can compose this function with one that returns a
//. Response, and reject any responses that don't meet the expected status
//. code.
//.
//. In combination with [`responseToError`](#responseToError), you can then
//. flatten it back into the outer Future. The usage example under the
//. [Http](#http) section shows this.
export const acceptStatus = code => matchStatus (reject) ({[code]: resolve});
//# bufferMessage :: Charset -> IncomingMessage -> Future Error String
//.
//. A version of [`buffer`](#buffer) specialized in [IncomingMessage][]s.
//.
//. See also [`bufferResponse`](#bufferResponse) and
//. [`autoBufferMessage`](#autoBufferMessage).
//.
//. Given a charset and an IncomingMessage, returns a Future with the buffered,
//. encoded, message body.
export const bufferMessage = charset => message => (
mapRej (e => new Error ('Failed to buffer response: ' + e.message))
(bufferString (charset) (message))
);
//# bufferResponse :: Charset -> Response -> Future Error String
//.
//. A composition of [`Response.message`](#Response.message) and
//. [`bufferMessage`](#bufferMessage) for your convenience.
//.
//. See also [autoBufferResponse](#autoBufferResponse).
export const bufferResponse = charset => response => (
bufferMessage (charset) (Response.message (response))
);
//# autoBufferMessage :: IncomingMessage -> Future Error String
//.
//. Given an IncomingMessage, buffers and decodes the message body using the
//. charset provided in the message headers. Falls back to UTF-8 if the
//. charset was not provided.
//.
//. Returns a Future with the buffered, encoded, message body.
//.
//. See also [bufferMessage](#bufferMessage).
export const autoBufferMessage = message => {
const contentType = message.headers['content-type'] || defaultContentType;
const parsed = charsetRegex.exec (contentType);
const charset = parsed == null ? defaultCharset : parsed[1];
return bufferMessage (charset) (message);
};
//# autoBufferResponse :: Response -> Future Error String
//.
//. A composition of [`Response.message`](#Response.message) and
//. [`autoBufferMessage`](#autoBufferMessage) for your convenience.
//.
//. See also [bufferResponse](#bufferResponse).
export const autoBufferResponse = response => (
autoBufferMessage (Response.message (response))
);
//# responseToError :: Response -> Future Error a
//.
//. Given a [Response](#Response), returns a *rejected* Future of an instance
//. of Error with a message based on the content of the response.
export const responseToError = response => {
const message = Response.message (response);
return autoBufferMessage (message)
.pipe (chain (body => reject (new Error (
`Unexpected ${message.statusMessage} (${message.statusCode}) response. ` +
`Response body:\n\n${body.split ('\n').map (x => ` ${x}`).join ('\n')}`
))));
};
//. [`process.nextTick`]: https://nodejs.org/api/process.html#process_process_nexttick_callback_args
//. [`setImmediate`]: https://nodejs.org/api/timers.html#timers_setimmediate_callback_args
//. [`S.parseJson`]: https://sanctuary.js.org/#parseJson
//. [`Fluture.encase`]: https://github.com/fluture-js/Fluture#encase
//. [`JSON.parse`]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
//. [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer
//. [Fluture]: https://github.com/fluture-js/Fluture
//. [http options]: https://nodejs.org/api/http.html#http_http_request_url_options_callback
//. [IncomingMessage]: https://nodejs.org/api/http.html#http_class_http_incomingmessage
//. [Readable]: https://nodejs.org/api/stream.html#stream_class_stream_readable
//. [RFC2616 Section 10.3]: https://tools.ietf.org/html/rfc2616#section-10.3
//. [esm]: https://github.com/standard-things/esm