Skip to content

bricss/rekwest

Repository files navigation

The robust request library that humanity deserves 🌐

This package provides highly likely functional and easy-to-use abstraction atop of native http(s).request and http2.request.

Abstract

  • Fetch-alike
  • Cool-beans 🫐 config options (with defaults)
  • Automatic HTTP/2 support (ALPN negotiation)
  • Automatic or opt-in body parse (with non-UTF-8 charset decoding)
  • Automatic and simplistic Cookies treatment (with built-in jar & ttl)
  • Automatic decompression (with opt-in body compression)
  • Built-in streamable FormData interface
  • Support redirects & retries with fine-grained tune-ups
  • Support all legit request body types (include blobs & streams)
  • Support both CJS and ESM module systems
  • Fully promise-able and pipe-able
  • Zero dependencies

Prerequisites

  • Node.js >= 18.13.0

Installation

npm install rekwest --save

Usage

import rekwest, { constants } from 'rekwest';

const {
  HTTP2_HEADER_AUTHORIZATION,
  HTTP2_HEADER_CONTENT_ENCODING,
  HTTP2_METHOD_POST,
  HTTP_STATUS_OK,
} = constants;

const url = 'https://somewhe.re/somewhat/endpoint';

const res = await rekwest(url, {
  body: { celestial: 'payload' },
  headers: {
    [HTTP2_HEADER_AUTHORIZATION]: 'Bearer [token]',
    [HTTP2_HEADER_CONTENT_ENCODING]: 'br',  // enables: body compression
    /** [HTTP2_HEADER_CONTENT_TYPE]
     * is undue for
     * Array/Blob/File/FormData/Object/URLSearchParams body types
     * and will be set automatically, with an option to override it here
     */
  },
  method: HTTP2_METHOD_POST,
});

console.assert(res.statusCode === HTTP_STATUS_OK);
console.info(res.headers);
console.log(res.body);

import { Readable } from 'node:stream';
import rekwest, {
  constants,
  Blob,
  File,
  FormData,
} from 'rekwest';

const {
  HTTP2_HEADER_AUTHORIZATION,
  HTTP2_HEADER_CONTENT_ENCODING,
  HTTP2_METHOD_POST,
  HTTP_STATUS_OK,
} = constants;

const blob = new Blob(['bits']);
const file = new File(['bits'], 'file.dab');
const readable = Readable.from('bits');

const fd = new FormData({
  aux: Date.now(),  // either [[key, value]] or kv sequenceable
});

fd.append('celestial', 'payload');
fd.append('blob', blob, 'blob.dab');
fd.append('file', file);
fd.append('readable', readable, 'readable.dab');

const url = 'https://somewhe.re/somewhat/endpoint';

const res = await rekwest(url, {
  body: fd,
  headers: {
    [HTTP2_HEADER_AUTHORIZATION]: 'Bearer [token]',
    [HTTP2_HEADER_CONTENT_ENCODING]: 'br',  // enables: body compression
  },
  method: HTTP2_METHOD_POST,
});

console.assert(res.statusCode === HTTP_STATUS_OK);
console.info(res.headers);
console.log(res.body);

API

rekwest(url[, options])

  • url {string | URL} The URL to send the request to
  • options {Object} Extends http(s).RequestOptions along with extra http2.ClientSessionOptions & http2.ClientSessionRequestOptions and tls.ConnectionOptions for HTTP/2 attunes
    • baseURL {string | URL} The base URL to use in cases where url is a relative URL
    • body {string | Array | ArrayBuffer | ArrayBufferView | AsyncIterator | Blob | Buffer | DataView | File | FormData | Iterator | Object | Readable | ReadableStream | SharedArrayBuffer | URLSearchParams} The body to send with the request
    • cookies {boolean | Array<[k, v]> | Array<string> | Cookies | Object | URLSearchParams} Default: true The cookies to add to the request
    • cookiesTTL {boolean} Default: false Controls enablement of TTL for the cookies cache
    • credentials {include | omit | same-origin} Default: same-origin Controls credentials in case of cross-origin redirects
    • digest {boolean} Default: true Controls whether to read the response stream or simply add a mixin
    • follow {number} Default: 20 The number of redirects to follow
    • h2 {boolean} Default: false Forces the use of HTTP/2 protocol
    • headers {Object} The headers to add to the request
    • maxRetryAfter {number} The upper limit of retry-after header. If unset, it will use timeout value
    • parse {boolean} Default: true Controls whether to parse response body or simply return a buffer
    • redirect {error | follow | manual} Default: follow Controls the redirect flows
    • retry {Object} Represents the retry options
      • attempts {number} Default: 0 The number of retry attempts
      • backoffStrategy {string} Default: interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E) The backoff strategy algorithm that increases logarithmically. To fixate set value to interval * 1
      • errorCodes {string[]} Default: ['EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'EHOSTDOWN', 'EHOSTUNREACH', 'ENETDOWN', 'ENETUNREACH', 'ENOTFOUND', 'EPIPE', 'ERR_HTTP2_STREAM_ERROR'] The list of error codes to retry on
      • interval {number} Default: 1e3 The initial retry interval
      • retryAfter {boolean} Default: true Controls retry-after header receptiveness
      • statusCodes {number[]} Default: [429, 500, 502, 503, 504] The list of status codes to retry on
    • stripTrailingSlash {boolean} Default: false Controls whether to strip trailing slash at the end of the URL
    • thenable {boolean} Default: false Controls the promise resolutions
    • timeout {number} Default: 3e5 The number of milliseconds a request can take before termination
    • trimTrailingSlashes {boolean} Default: false Controls whether to trim trailing slashes within the URL
  • Returns: Promise that resolves to extended http.IncomingMessage or http2.ClientHttp2Stream which is respectively readable and duplex streams
    • if digest: true & parse: true
      • body {string | Array | Buffer | Object} The body based on its content type
    • if digest: false
      • arrayBuffer {AsyncFunction} Reads the response and returns ArrayBuffer
      • blob {AsyncFunction} Reads the response and returns Blob
      • body {AsyncFunction} Reads the response and returns Buffer if parse: false
      • json {AsyncFunction} Reads the response and returns Object
      • text {AsyncFunction} Reads the response and returns String
    • bodyUsed {boolean} Indicates whether the response were read or not
    • cookies {undefined | Cookies} The cookies sent and received with the response
    • headers {Object} The headers received with the response
    • httpVersion {string} Indicates protocol version negotiated with the server
    • ok {boolean} Indicates if the response was successful (statusCode: 200-299)
    • redirected {boolean} Indicates if the response is the result of a redirect
    • statusCode {number} Indicates the status code of the response
    • trailers {undefined | Object} The trailer headers received with the response

rekwest.defaults

The object to fulfill with default options.


rekwest.extend(options)

The method to extend default options per instance.

import rekwest, { constants } from 'rekwest';

const {
  HTTP_STATUS_OK,
} = constants;

const rk = rekwest.extend({
  baseURL: 'https://somewhe.re',
});

const signal = AbortSignal.timeout(1e4);
const url = '/somewhat/endpoint';

const res = await rk(url, {
  signal,
});

console.assert(res.statusCode === HTTP_STATUS_OK);
console.info(res.headers);
console.log(res.body);

rekwest.stream(url[, options])

The method with limited functionality to use with streams and/or pipes.

  • No automata (redirects & retries)
  • Pass h2: true in options to use HTTP/2 protocol
    • Use ackn({ url: URL }) method in advance to check the available protocols
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
import rekwest, {
  ackn,
  constants,
} from 'rekwest';

const {
  HTTP2_METHOD_POST,
} = constants;

const url = new URL('https://somewhe.re/somewhat/endpoint');
const options = await ackn({ url });

await pipeline(
  fs.createReadStream('/path/to/read/inlet.xyz'),
  rekwest.stream(url, { ...options, method: HTTP2_METHOD_POST }),
  fs.createWriteStream('/path/to/write/outlet.xyz'),
);

For more details, please check tests (coverage: >97%) in the repository.