Fluture Node
FP-style HTTP and streaming utils for Node based on Fluture.
Skip to the Http section for the main code example.
Usage
$ 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
EventEmitter
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.
> const emitter = new EventEmitter ();
> setTimeout (() => emitter.emit ('answer', 42), 100);
> once ('answer') (emitter);
Future.of (42);
Buffer
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.
> encode ('utf8') (Buffer.from ('Hello world!'));
'Hello world!'
Stream
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.
A Readable stream which ends after emiting zero bytes. Can be useful
as an empty Request
body, for example.
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.
> 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')]);
A version of buffer
specialized in Strings.
Takes a charset and a Readable stream of Buffers, and returns
a Future containing a String with the fully buffered and encoded result.
Event Loop
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.
> instant ('noodles')
Future.of ('noodles')
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.
> immediate ('results')
Future.of ('results')
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.
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:
- log
"Aldwin Vlasblom"
to the terminal if nothing weird happens; or - Report a 404 error using the message returned from the server; or
- 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
with a maxmum of 20 redirects, but we could have used a different
redirection function using followRedirectsWith
with the aggressiveRedirectionPolicy
or
even a fully custom policy.
-
How an unexpected status was treated: We passed in a handler to
matchStatus
.
We used responseToError
, conviently provided by
this library, but we could have used a custom mechanism.
-
How responses with expected status codes are treated:
The 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
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
or even just 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.
Constructs a value of type Request to be used as an argument for
functions such as sendRequest
.
Takes the following arguments:
- 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. - A String containing the request URL.
- A Future of a Readable stream of Buffers 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
for a usage example.
Get the options out of a Request.
Get the url out of a Request.
Get the body out of a Request.
Constructs a value of type Response. These values are typically created
for you by functions such as sendRequest
.
Takes the following arguments:
- A Request.
- An IncomingMessage assumed to belong to the Request.
Get the request out of a Response.
Get the message out of a 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
: Make a generic HTTP request.retrieve
: Make a GET request.
Given a Request, returns a Future which makes an HTTP request
and resolves with the resulting Response.
If the Future is cancelled, the request is aborted.
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".
A version of 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.
retrieve ('https://api.github.com/users/Avaq') ({'User-Agent': 'Avaq'})
A version of sendRequest
for sending arbitrary data to
a server. There's also more specific versions for sending common types of
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.
A version of 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.
sendJson ('PUT')
('https://example.com/users/bob')
({Authorization: 'Bearer asd123'})
({name: 'Bob', email: 'bob@example.com'});
A version of 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.
sendForm ('POST')
('https://example.com/users/create')
({})
({name: 'Bob', email: 'bob@example.com'});
Transform a Response
based on its status code.
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:
import {reject} from 'fluture';
matchStatus (processResponse) ({
404: () => reject (new Error ('Example not found!')),
});
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
and
the aggressiveRedirectionPolicy
.
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
.
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
and
the aggressiveRedirectionPolicy
.
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
.
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
function. You can extend it, using
matchStatus
to create a custom redirection policy, as
shown in the example:
See also aggressiveRedirectionPolicy
.
const redirectToBestOption = () => {
};
const myRedirectionPolicy = matchStatus (defaultRedirectionPolicy) ({
300: redirectToBestOption,
301: redirectUsingGetMethod,
});
retrieve ('https://example.com') ({})
.pipe (chain (followRedirectsWith (myRedirectionPolicy) (10)))
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
.
retrieve ('https://example.com') ({})
.pipe (chain (followRedirectsWith (aggressiveRedirectionPolicy) (10)))
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
for an out-of-the-box redirect-
follower. See aggressiveRedirectionPolicy
and defaultRedirectionPolicy
for
additional usage examples.
Given the maximum numbers of redirections, follows redirects according to
the default redirection policy.
See the Http section for a usage example.
This function "tags" a 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
, 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
, you can then
flatten it back into the outer Future. The usage example under the
Http section shows this.
A version of buffer
specialized in IncomingMessages.
See also bufferResponse
and
autoBufferMessage
.
Given a charset and an IncomingMessage, returns a Future with the buffered,
encoded, message body.
A composition of Response.message
and
bufferMessage
for your convenience.
See also autoBufferResponse.
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.
A composition of Response.message
and
autoBufferMessage
for your convenience.
See also bufferResponse.
Given a Response, returns a rejected Future of an instance
of Error with a message based on the content of the response.