257 lines
8.1 KiB
JavaScript
257 lines
8.1 KiB
JavaScript
|
const browser = typeof window !== 'undefined';
|
||
|
const querystring = require('querystring');
|
||
|
const transport = browser ? require('./browser') : require('./node');
|
||
|
|
||
|
/**
|
||
|
* Snekfetch
|
||
|
* @extends Stream.Readable
|
||
|
* @extends Promise
|
||
|
*/
|
||
|
class Snekfetch extends transport.Extension {
|
||
|
/**
|
||
|
* 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} [followRedirects=true] If the request should follow redirects
|
||
|
* @property {object} [qs=querystring] Querystring module to use, any object providing
|
||
|
* `stringify` and `parse` for querystrings
|
||
|
* @property {number} [version = 1] The http version to use [1 or 2]
|
||
|
* @property {external:Agent} [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({ version: 1, qs: querystring, followRedirects: true }, opts);
|
||
|
this.request = transport.buildRequest.call(this, method, url, opts);
|
||
|
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.request.query)
|
||
|
this.request.query = {};
|
||
|
if (name !== null && typeof name === 'object') {
|
||
|
for (const [k, v] of Object.entries(name))
|
||
|
this.query(k, v);
|
||
|
} else {
|
||
|
this.request.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 (name !== null && typeof name === 'object') {
|
||
|
for (const key of Object.keys(name))
|
||
|
this.set(key, name[key]);
|
||
|
} else {
|
||
|
this.request.setHeader(name, 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.data instanceof transport.FormData ? this.data : this.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.data = data;
|
||
|
} else if (data !== null && typeof data === 'object') {
|
||
|
const header = this.request.getHeader('content-type');
|
||
|
let serialize;
|
||
|
if (header) {
|
||
|
if (header.includes('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.data = serialize(data);
|
||
|
} else {
|
||
|
this.data = data;
|
||
|
}
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
then(resolver, rejector) {
|
||
|
if (this._response)
|
||
|
return this._response.then(resolver, rejector);
|
||
|
// eslint-disable-next-line no-return-assign
|
||
|
return this._response = transport.finalizeRequest.call(this)
|
||
|
.then(({ response, raw, redirect, headers }) => {
|
||
|
if (redirect) {
|
||
|
let method = this.request.method;
|
||
|
if ([301, 302].includes(response.statusCode)) {
|
||
|
if (method !== 'HEAD')
|
||
|
method = 'GET';
|
||
|
this.data = null;
|
||
|
} else if (response.statusCode === 303) {
|
||
|
method = 'GET';
|
||
|
}
|
||
|
|
||
|
const redirectHeaders = this.request.getHeaders();
|
||
|
delete redirectHeaders.host;
|
||
|
return new Snekfetch(method, redirect, {
|
||
|
data: this.data,
|
||
|
headers: redirectHeaders,
|
||
|
version: this.options.version,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const statusCode = response.statusCode || response.status;
|
||
|
// 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 {string} text Raw response body
|
||
|
* @prop {boolean} ok If the response code is >= 200 and < 300
|
||
|
* @prop {number} status HTTP status code
|
||
|
* @prop {string} statusText Human readable HTTP status
|
||
|
*/
|
||
|
const res = {
|
||
|
request: this.request,
|
||
|
get body() {
|
||
|
delete res.body;
|
||
|
const type = this.headers['content-type'];
|
||
|
if (type && type.includes('application/json')) {
|
||
|
try {
|
||
|
res.body = JSON.parse(res.text);
|
||
|
} catch (err) {
|
||
|
res.body = res.text;
|
||
|
}
|
||
|
} else if (type && type.includes('application/x-www-form-urlencoded')) {
|
||
|
res.body = self.options.qs.parse(res.text);
|
||
|
} else {
|
||
|
res.body = raw;
|
||
|
}
|
||
|
|
||
|
return res.body;
|
||
|
},
|
||
|
text: raw.toString(),
|
||
|
ok: statusCode >= 200 && statusCode < 400,
|
||
|
headers: headers || response.headers,
|
||
|
status: statusCode,
|
||
|
statusText: response.statusText || transport.STATUS_CODES[response.statusCode],
|
||
|
};
|
||
|
|
||
|
if (res.ok) {
|
||
|
return res;
|
||
|
} else {
|
||
|
const err = new Error(`${res.status} ${res.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.status ? err : null) : Promise.reject(err)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
_finalizeRequest() {
|
||
|
if (!this.request)
|
||
|
return;
|
||
|
|
||
|
if (this.request.method !== 'HEAD')
|
||
|
this.set('Accept-Encoding', 'gzip, deflate');
|
||
|
if (this.data && this.data.getBoundary)
|
||
|
this.set('Content-Type', `multipart/form-data; boundary=${this.data.getBoundary()}`);
|
||
|
|
||
|
if (this.request.query) {
|
||
|
const [path, query] = this.request.path.split('?');
|
||
|
this.request.path = `${path}?${this.options.qs.stringify(this.request.query)}${query ? `&${query}` : ''}`;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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.concat('BREW').filter((m) => m !== 'M-SEARCH');
|
||
|
for (const method of Snekfetch.METHODS) {
|
||
|
Snekfetch[method.toLowerCase()] = function runMethod(url, opts) {
|
||
|
const Constructor = 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}
|
||
|
*/
|