UNPKG

8.55 kBJavaScriptView Raw
1const browser = typeof window !== 'undefined';
2const querystring = require('querystring');
3const Package = require('../package');
4const transport = browser ? require('./browser') : require('./node');
5
6/**
7 * Snekfetch
8 * @extends Stream.Readable
9 * @extends Promise
10 */
11class Snekfetch extends transport.Extension {
12 /**
13 * Options to pass to the Snekfetch constructor
14 * @typedef {object} snekfetchOptions
15 * @memberof Snekfetch
16 * @property {object} [headers] Headers to initialize the request with
17 * @property {object|string|Buffer} [data] Data to initialize the request with
18 * @property {string|Object} [query] Query to intialize the request with
19 * @property {boolean} [followRedirects=true] If the request should follow redirects
20 * @property {object} [qs=querystring] Querystring module to use, any object providing
21 * `stringify` and `parse` for querystrings
22 * @property {number} [version = 1] The http version to use [1 or 2]
23 * @property {external:Agent} [agent] Whether to use an http agent
24 */
25
26 /**
27 * Create a request.
28 * Usually you'll want to do `Snekfetch#method(url [, options])` instead of
29 * `new Snekfetch(method, url [, options])`
30 * @param {string} method HTTP method
31 * @param {string} url URL
32 * @param {Snekfetch.snekfetchOptions} opts Options
33 */
34 constructor(method, url, opts = {}) {
35 super();
36 this.options = Object.assign({ version: 1, qs: querystring, followRedirects: true }, opts);
37 this.request = transport.buildRequest.call(this, method, url, opts);
38 if (opts.query) this.query(opts.query);
39 if (opts.data) this.send(opts.data);
40 }
41
42 /**
43 * Add a query param to the request
44 * @param {string|Object} name Name of query param or object to add to query
45 * @param {string} [value] If name is a string value, this will be the value of the query param
46 * @returns {Snekfetch} This request
47 */
48 query(name, value) {
49 this._checkModify();
50 if (!this.request.query) this.request.query = {};
51 if (name !== null && typeof name === 'object') {
52 for (const [k, v] of Object.entries(name)) this.query(k, v);
53 } else {
54 this.request.query[name] = value;
55 }
56 return this;
57 }
58
59 /**
60 * Add a header to the request
61 * @param {string|Object} name Name of query param or object to add to headers
62 * @param {string} [value] If name is a string value, this will be the value of the header
63 * @returns {Snekfetch} This request
64 */
65 set(name, value) {
66 this._checkModify();
67 if (name !== null && typeof name === 'object') {
68 for (const key of Object.keys(name)) this.set(key, name[key]);
69 } else {
70 this.request.setHeader(name, value);
71 }
72 return this;
73 }
74
75 /**
76 * Attach a form data object
77 * @param {string} name Name of the form attachment
78 * @param {string|Object|Buffer} data Data for the attachment
79 * @param {string} [filename] Optional filename if form attachment name needs to be overridden
80 * @returns {Snekfetch} This request
81 */
82 attach(...args) {
83 this._checkModify();
84 const form = this._getFormData();
85 if (typeof args[0] === 'object') {
86 for (const [k, v] of Object.entries(args[0])) this.attach(k, v);
87 } else {
88 form.append(...args);
89 }
90 return this;
91 }
92
93 /**
94 * Send data with the request
95 * @param {string|Buffer|Object} data Data to send
96 * @returns {Snekfetch} This request
97 */
98 send(data) {
99 this._checkModify();
100 if (data instanceof transport.FormData || transport.shouldSendRaw(data)) {
101 this.data = data;
102 } else if (data !== null && typeof data === 'object') {
103 const header = this.request.getHeader('content-type');
104 let serialize;
105 if (header) {
106 if (header.includes('json')) serialize = JSON.stringify;
107 else if (header.includes('urlencoded')) serialize = this.options.qs.stringify;
108 } else {
109 this.set('Content-Type', 'application/json');
110 serialize = JSON.stringify;
111 }
112 this.data = serialize(data);
113 } else {
114 this.data = data;
115 }
116 return this;
117 }
118
119 then(resolver, rejector) {
120 if (this._response) return this._response.then(resolver, rejector);
121 // eslint-disable-next-line no-return-assign
122 return this._response = transport.finalizeRequest.call(this)
123 .then(({ response, raw, redirect, headers }) => {
124 if (redirect) {
125 let method = this.request.method;
126 if ([301, 302].includes(response.statusCode)) {
127 if (method !== 'HEAD') method = 'GET';
128 this.data = null;
129 } else if (response.statusCode === 303) {
130 method = 'GET';
131 }
132
133 const redirectHeaders = this.request.getHeaders();
134 delete redirectHeaders.host;
135 return new Snekfetch(method, redirect, {
136 data: this.data,
137 headers: redirectHeaders,
138 });
139 }
140
141 const statusCode = response.statusCode || response.status;
142 // forgive me :(
143 const self = this; // eslint-disable-line consistent-this
144 /**
145 * Response from Snekfetch
146 * @typedef {Object} SnekfetchResponse
147 * @memberof Snekfetch
148 * @prop {HTTP.Request} request
149 * @prop {?string|object|Buffer} body Processed response body
150 * @prop {string} text Raw response body
151 * @prop {boolean} ok If the response code is >= 200 and < 300
152 * @prop {number} status HTTP status code
153 * @prop {string} statusText Human readable HTTP status
154 */
155 const res = {
156 request: this.request,
157 get body() {
158 delete res.body;
159 const type = this.headers['content-type'];
160 if (type && type.includes('application/json')) {
161 try {
162 res.body = JSON.parse(res.text);
163 } catch (err) {
164 res.body = res.text;
165 }
166 } else if (type && type.includes('application/x-www-form-urlencoded')) {
167 res.body = self.options.qs.parse(res.text);
168 } else {
169 res.body = raw;
170 }
171
172 return res.body;
173 },
174 text: raw.toString(),
175 ok: statusCode >= 200 && statusCode < 400,
176 headers: headers || response.headers,
177 status: statusCode,
178 statusText: response.statusText || transport.STATUS_CODES[response.statusCode],
179 };
180
181 if (res.ok) {
182 return res;
183 } else {
184 const err = new Error(`${res.status} ${res.statusText}`.trim());
185 Object.assign(err, res);
186 return Promise.reject(err);
187 }
188 })
189 .then(resolver, rejector);
190 }
191
192 catch(rejector) {
193 return this.then(null, rejector);
194 }
195
196 /**
197 * End the request
198 * @param {Function} [cb] Optional callback to handle the response
199 * @returns {Promise} This request
200 */
201 end(cb) {
202 return this.then(
203 (res) => cb ? cb(null, res) : res,
204 (err) => cb ? cb(err, err.status ? err : null) : Promise.reject(err)
205 );
206 }
207
208 _getFormData() {
209 if (!(this.data instanceof transport.FormData)) {
210 this.data = new transport.FormData();
211 }
212 return this.data;
213 }
214
215 _finalizeRequest() {
216 if (!this.request) return;
217 if (!this.request.getHeader('user-agent')) {
218 this.set('User-Agent', `snekfetch/${Snekfetch.version} (${Package.homepage})`);
219 }
220 if (this.request.method !== 'HEAD') this.set('Accept-Encoding', 'gzip, deflate');
221 if (this.data && this.data.getBoundary) {
222 this.set('Content-Type', `multipart/form-data; boundary=${this.data.getBoundary()}`);
223 }
224 if (this.request.query) {
225 const [path, query] = this.request.path.split('?');
226 this.request.path = `${path}?${this.options.qs.stringify(this.request.query)}${query ? `&${query}` : ''}`;
227 }
228 }
229
230 _checkModify() {
231 if (this.response) throw new Error('Cannot modify request after it has been sent!');
232 }
233}
234
235Snekfetch.version = Package.version;
236
237/**
238 * Create a ((THIS)) request
239 * @dynamic this.METHODS
240 * @method Snekfetch.((THIS)lowerCase)
241 * @param {string} url The url to request
242 * @param {Snekfetch.snekfetchOptions} [opts] Options
243 * @returns {Snekfetch}
244 */
245Snekfetch.METHODS = transport.METHODS.concat('BREW').filter((m) => m !== 'M-SEARCH');
246for (const method of Snekfetch.METHODS) {
247 Snekfetch[method.toLowerCase()] = (url, opts) => new Snekfetch(method, url, opts);
248}
249
250module.exports = Snekfetch;
251
252/**
253 * @external Agent
254 * @see {@link https://nodejs.org/api/http.html#http_class_http_agent}
255 */