UNPKG

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