Beggar
Preamble
Beggar is heavily inspired by mikael's request module.
Every other http client library I tried always left me wanting and I would always come back to request.
In my opinion, what request did better than any other http client library was its stream interface.
request('http://localhost:3000/myfile.txt').pipe(fs.createWriteStream('./filesystem/file.txt'));
Even streaming the body into the request.
const { pipeline } = require('stream');
pipeline(
fs.createReadStream('./data.csv'),
request({ method: 'post', uri: 'http://localhost:3000/upload' }),
process.stdout,
err => console.error(err.message)
);
It was a great abstraction over NodeJS's http.ClientRequest and http.IncomingMessage objects.
In contrast what it struggled more than any other http client library was at adapting to javascript's Promise API.
Nobody wants to write this over and over:
const { promisify } = require('util');
const request = promisify(require('request'));
You lose access to utility functions such as request.post(...)
and request.put(...)
when you promisify over the module.
Modules like request-promise partially solve this issue but then we lose the stream interface.
Goal
Beggar aims to offer a similar API to Mikael's request and to remain true to its bidirectional streaming support, whilst
being promise compatible and play well with async/await natively.
I would like to keep the module as thin a wrapper over NodeJS's http.ClientRequest and http.IncomingMessage as possible.
Supported features
- Stream support
- Promise support via
then
and catch
- Implicit parsing of response body (json, string, buffer)
- form and multipart-form requests
- Specify number of maximum redirections
- Basic Auth
- Automatic decompression of gzip, deflate and br compressions
- Can reject non 2xx statusCode responses automatically
- Proxying support for http proxies
- Request Cancelation
- Extending with user provided default options
- Simple mode where responses are resolved to their body and errors are rejected
Usage
Supported Options
- method
string
(must be HTTP verb) - uri
string
or URL
- path
string
overrides the path component of the uri. Useful when creating a beggar instance with a default uri and simply updating the path. - headers
object
object containing headers - body
Buffer
| string
| Readable
if body is a string or buffer the body will be written to the underlying request request as is. Other types will be stringified and sent as JSON with the Content-Type header set to application/json
. - form
object
will be url-encoded using the qs library and sent with Content-Type header set to application/x-www-form-urlencoded
- formData
object
will be used with form-data library to create a multi-part from request - qs
object
will use qs library to generate appropriate query. Has precedence over option.query
. If query string is part of options.uri
it will only write over the fields it defines but preserve the rest - query
object
same as options.qs
but will use the NodeJS native querystring module. - auth
object
object containg username and password used for Basic Authentication
- maxRedirects
number
the maximum number of redirects for begger to follow - followAllRedirects
boolean
will follow all redirects if true and maxRedirects not specified - decompress
boolean
by default true. Will decompress encodings br,gzip, and deflate. Set to false to get raw buffer - agent
http.Agent
or false
, will be passed to underlying NodeJS ClientRequest - proxy
string
| URL
| { uri: string
| URL
; tls: object
} Uri of the http proxy server. tls
options specific to the proxy can be passed here as well. - rejectError
boolean
default false. Will reject an error containing response headers, body, statusCode and message on statusCodes outside of the 2xx range - simple
boolean
default false. When true will resolve the body without the response object. In this mode if a non 2XX range status response is returned it shall be rejected regardless of whether rejectError is set to false. - raw
boolean
default false. If true will bypass Beggars implicity body parsing and response.body will be a Buffer instance - tls
object
tls options that will be passed to https.request. For more documentation on tls options please read the official NodeJS documentation https://nodejs.org/api/https.html#https_https_request_options_callback
The request function supports two signatures:
beggar(options);
and
beggar(uri, options);
In the latter the uri can be either a string
or a URL
object and will take precedence over options.uri
For convenience all http verb methods defined in http.METHODS are attached to the request function object and will take precedence over options.method
beggar.get(uri);
beggar.post(uri, { body: 'data' });
Examples
Bring begger's request function into scope:
const { beggar } = require('beggar');
Basic Usage
Using the native http Incoming message object
const req = beggar.post('http://localhost:3000');
req.write('some data');
req.end();
req.on('response', response => {
});
Using the stream interface (same as Mikael's request)
pipeline(
beggar.get('http://example.com/file.txt'),
beggar.post('http://bucket.com/upload'),
fs.createWriteStream('./response.json'),
err => { ... }
)
Using promises and async/await
const response = await beggar.get('http://localhost:3000');
(note) for those interested: beggar(..) does not return an instance of a Promise, However it is thenable/catchable and therefore async/await compliant.
Mixing them together
const file = fs.createReadStream('./file.txt');
const request = file.pipe(beggar.put('http://destination.com'));
const response = await request;
beggar.get(uri, { headers: { 'Accept-Encoding': 'application/json' } });
Sending request with a body
beggar.post({
uri: 'https://example.com/upload',
headers: { 'Content-Type': 'text/plain' },
body: 'my string payload that could equally be a buffer',
});
beggar.put({
uri: 'https://example.com/resource/1',
body: { resource: 'values' },
});
beggar.post({
uri: 'https://example.com/fileUpload',
body: fs.createReadStream('./path/to/file'),
});
Sending forms
Beggar will automatically send form-encode the body and set the appropriate Content-Type when the body is sent via the form option.
beggar.post({
uri: 'https://example.com/form',
form: { key: 'value', key2: 'value2' },
});
Multipart form data
Beggar uses formData under the hood to generate multipart requests. Simply provide an object where the keys will be interpreted as name and filename and the values the body of each part.
beggar.post({
uri: 'https://example.com/form',
formData: { key: 'value' },
});
This will write something similar to the request:
----------------------------593029851590825188224183
Content-Disposition: form-data; name="key"; filename="key"
value
----------------------------593029851590825188224183--
Query Strings
Beggar lets you override the given query string programmatically. The next example shall only override the given fields. The qs option uses the qs library for query string encoding. The query options uses the native NodeJS querystring module.
beggar.get({
uri: 'https://example.com?override=willBeOverwritten&stable=willStayAsIs',
qs: { override: 'new value' },
);
beggar.get({
uri: 'https://example.com?override=willBeOverwritten&stable=willStayAsIs',
query: { override: 'new value' },
);
Basic Authentication
beggar.get('https://protected.com', { auth: { user: 'username', pass: 'password' } });
Http Proxies
Simply supply the proxy uri and beggar will handle the proxied request. Provide the proxy's basic auth within the URI and it shall be used for Proxy-Authorization Header on proxy connect request.
beggar.get({
uri: 'https://server.com',
proxy: 'http://username:password@proxy.com:2345',
});
Request Cancellation
A beggar connection/request object can be cancelled. This will abort the underlying request. Once aborted the beggar request will emit abort
and error
with CancelError.
const { beggar, CancelError } = require('beggar');
const request = beggar.get('https://example.com');
request.cancel();
request.once('abort', () => console.log('Request aborted'));
request.once('error', err => {
console.log(err.message);
console.log(err instanceof CancelError);
console.log(request.isCancelled);
});
request
.then(resp => {
})
.catch(err => {
});
Simple mode
Often when dealing with requests we simply want the return value of the body and not the entire response object with headers and statusCodes.
In this mode non 2XX statusCode responses will be rejected regardless of whether options.rejectError
is explicitly set to false.
const body = await beggar.get('https://www.example.com', { simple: true });
By default simple is false, and beggar will return a response object. You can override this with a new defaulted beggar instance
const request = beggar.defaults({ simple: true });
const body = request.get('https://www.google.com');
Raw mode
By default Beggar shall apply JSON.parse to a response body where the response Content-Type is application/json, or return the string value of the buffer if the Content-Type is text/*. To escape this behaviour and simply have the returned body always be a buffer you can set options.raw
to true.
const buffer = await beggar.get('https://www.google.com', { simple: true, raw: true });
Decompression
By default beggar will automatically decompress payloads that have been compressed by gzip, br, or deflate. This can be deactivated by setting options.decompress
to false.
const response = await beggar.get('https//httpbin.org/brotli', { decompress: false });
Note that if decompress is false but the content is not encoded Beggar will continue to implicitly parse the response. If the content is encoded or invalid encodings are contained in the Content-Encoding header, Beggar will simply set the body as a Buffer.
Defaults
Beggar also supports creating new instance of request with default options.
const authenticatedRequest = beggar.defaults({ auth: { user: 'username', pass: 'password' } });
authenticatedRequest.post('https://myservice.com/upload', { body: 'data' });
authenticatedRequest.post({
uri: 'https://myservice.com/upload',
body: 'data',
auth: { user: 'differentUser', pass: 'differentPassword' },
});
Note that the defaults
utility only exists on the root beggar function and not on the functions created by
defaults.
RoadMap
There are some things that could be improved upon. A few that come to mind:
- multi-part formdata options
- cookie support (if requested)
- XML parsing support
Contributing
Contributions and feedback are more than welcome. If this never becomes more than a small personal project I am happy with that too, and thank anybody who takes the time look at it or give feedback.