UNPKG

snekfetch

Version:

Just do http requests without all that weird nastiness from other libs

261 lines (240 loc) 7.84 kB
'use strict'; const transport = require(typeof window !== 'undefined' ? './browser' : './node'); /** * Snekfetch * @extends Stream.Readable * @extends Promise */ class Snekfetch extends transport.Parent { /** * Options to pass to the Snekfetch constructor * @typedef {object} SnekfetchOptions * @memberof Snekfetch * @property {object} [headers] Headers to initialize the request with * @property {object|string|Buffer} [data] Data to initialize the request with * @property {string|Object} [query] Query to intialize the request with * @property {boolean} [redirect='follow'] If the request should follow redirects * @property {object} [qs=querystring] Querystring module to use, any object providing * `stringify` and `parse` for querystrings * @property {external:Agent|boolean} [agent] Whether to use an http agent */ /** * Create a request. * Usually you'll want to do `Snekfetch#method(url [, options])` instead of * `new Snekfetch(method, url [, options])` * @param {string} method HTTP method * @param {string} url URL * @param {SnekfetchOptions} [opts] Options */ constructor(method, url, opts = {}) { super(); this.options = Object.assign({ qs: transport.querystring, method, url, redirect: 'follow', }, opts, { headers: {}, query: undefined, data: undefined, }); if (opts.headers) { this.set(opts.headers); } if (opts.query) { this.query(opts.query); } if (opts.data) { this.send(opts.data); } } /** * Add a query param to the request * @param {string|Object} name Name of query param or object to add to query * @param {string} [value] If name is a string value, this will be the value of the query param * @returns {Snekfetch} This request */ query(name, value) { if (this.options.query === undefined) { this.options.query = {}; } if (typeof name === 'object') { Object.assign(this.options.query, name); } else { this.options.query[name] = value; } return this; } /** * Add a header to the request * @param {string|Object} name Name of query param or object to add to headers * @param {string} [value] If name is a string value, this will be the value of the header * @returns {Snekfetch} This request */ set(name, value) { if (typeof name === 'object') { for (const [k, v] of Object.entries(name)) { this.options.headers[k.toLowerCase()] = v; } } else { this.options.headers[name.toLowerCase()] = value; } return this; } /** * Attach a form data object * @param {string} name Name of the form attachment * @param {string|Object|Buffer} data Data for the attachment * @param {string} [filename] Optional filename if form attachment name needs to be overridden * @returns {Snekfetch} This request */ attach(...args) { const form = this.options.data instanceof transport.FormData ? this.options.data : this.options.data = new transport.FormData(); if (typeof args[0] === 'object') { for (const [k, v] of Object.entries(args[0])) { this.attach(k, v); } } else { form.append(...args); } return this; } /** * Send data with the request * @param {string|Buffer|Object} data Data to send * @returns {Snekfetch} This request */ send(data) { if (data instanceof transport.FormData || transport.shouldSendRaw(data)) { this.options.data = data; } else if (data !== null && typeof data === 'object') { const header = this.options.headers['content-type']; let serialize; if (header) { if (header.includes('application/json')) { serialize = JSON.stringify; } else if (header.includes('urlencoded')) { serialize = this.options.qs.stringify; } } else { this.set('Content-Type', 'application/json'); serialize = JSON.stringify; } this.options.data = serialize(data); } else { this.options.data = data; } return this; } then(resolver, rejector) { if (this._response) { return this._response.then(resolver, rejector); } this._finalizeRequest(); // eslint-disable-next-line no-return-assign return this._response = transport.request(this) .then(({ raw, headers, statusCode, statusText }) => { // forgive me :( const self = this; // eslint-disable-line consistent-this /** * Response from Snekfetch * @typedef {Object} SnekfetchResponse * @memberof Snekfetch * @prop {HTTP.Request} request * @prop {?string|object|Buffer} body Processed response body * @prop {Buffer} raw Raw response body * @prop {boolean} ok If the response code is >= 200 and < 300 * @prop {number} statusCode HTTP status code * @prop {string} statusText Human readable HTTP status */ const res = { request: this.request, get body() { delete res.body; const type = res.headers['content-type']; if (raw instanceof ArrayBuffer) { raw = new window.TextDecoder('utf8').decode(raw); // eslint-disable-line no-undef } if (/application\/json/.test(type)) { try { res.body = JSON.parse(raw); } catch (err) { res.body = String(raw); } } else if (/application\/x-www-form-urlencoded/.test(type)) { res.body = self.options.qs.parse(String(raw)); } else { res.body = raw; } return res.body; }, raw, ok: statusCode >= 200 && statusCode < 400, headers, statusCode, statusText, }; if (res.ok) { return res; } const err = new Error(`${statusCode} ${statusText}`.trim()); Object.assign(err, res); return Promise.reject(err); }) .then(resolver, rejector); } catch(rejector) { return this.then(null, rejector); } /** * End the request * @param {Function} [cb] Optional callback to handle the response * @returns {Promise} This request */ end(cb) { return this.then( (res) => (cb ? cb(null, res) : res), (err) => (cb ? cb(err, err.statusCode ? err : null) : Promise.reject(err)), ); } _finalizeRequest() { if (this.options.method !== 'HEAD') { this.set('Accept-Encoding', 'gzip, deflate'); } if (this.options.data && this.options.data.getBoundary) { this.set('Content-Type', `multipart/form-data; boundary=${this.options.data.getBoundary()}`); } if (this.options.query) { const [url, query] = this.options.url.split('?'); this.options.url = `${url}?${this.options.qs.stringify(this.options.query)}${query ? `&${query}` : ''}`; } } _read() { this.resume(); if (this._response) { return; } this.catch((err) => this.emit('error', err)); } } /** * Create a ((THIS)) request * @dynamic this.METHODS * @method Snekfetch.((THIS)lowerCase) * @param {string} url The url to request * @param {Snekfetch.snekfetchOptions} [opts] Options * @returns {Snekfetch} */ Snekfetch.METHODS = transport.METHODS.filter((m) => m !== 'M-SEARCH'); for (const method of Snekfetch.METHODS) { Snekfetch[method.toLowerCase()] = function runMethod(url, opts) { const Constructor = this && this.prototype instanceof Snekfetch ? this : Snekfetch; return new Constructor(method, url, opts); }; } module.exports = Snekfetch; /** * @external Agent * @see {@link https://nodejs.org/api/http.html#http_class_http_agent} */