UNPKG

9.85 kBJavaScriptView Raw
1
2/**
3 * Body.js
4 *
5 * Body interface provides common methods for Request and Response
6 */
7
8import Stream, {PassThrough} from 'node:stream';
9import {types, deprecate, promisify} from 'node:util';
10import {Buffer} from 'node:buffer';
11
12import Blob from 'fetch-blob';
13import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js';
14
15import {FetchError} from './errors/fetch-error.js';
16import {FetchBaseError} from './errors/base.js';
17import {isBlob, isURLSearchParameters} from './utils/is.js';
18
19const pipeline = promisify(Stream.pipeline);
20const INTERNALS = Symbol('Body internals');
21
22/**
23 * Body mixin
24 *
25 * Ref: https://fetch.spec.whatwg.org/#body
26 *
27 * @param Stream body Readable stream
28 * @param Object opts Response options
29 * @return Void
30 */
31export default class Body {
32 constructor(body, {
33 size = 0
34 } = {}) {
35 let boundary = null;
36
37 if (body === null) {
38 // Body is undefined or null
39 body = null;
40 } else if (isURLSearchParameters(body)) {
41 // Body is a URLSearchParams
42 body = Buffer.from(body.toString());
43 } else if (isBlob(body)) {
44 // Body is blob
45 } else if (Buffer.isBuffer(body)) {
46 // Body is Buffer
47 } else if (types.isAnyArrayBuffer(body)) {
48 // Body is ArrayBuffer
49 body = Buffer.from(body);
50 } else if (ArrayBuffer.isView(body)) {
51 // Body is ArrayBufferView
52 body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
53 } else if (body instanceof Stream) {
54 // Body is stream
55 } else if (body instanceof FormData) {
56 // Body is FormData
57 body = formDataToBlob(body);
58 boundary = body.type.split('=')[1];
59 } else {
60 // None of the above
61 // coerce to string then buffer
62 body = Buffer.from(String(body));
63 }
64
65 let stream = body;
66
67 if (Buffer.isBuffer(body)) {
68 stream = Stream.Readable.from(body);
69 } else if (isBlob(body)) {
70 stream = Stream.Readable.from(body.stream());
71 }
72
73 this[INTERNALS] = {
74 body,
75 stream,
76 boundary,
77 disturbed: false,
78 error: null
79 };
80 this.size = size;
81
82 if (body instanceof Stream) {
83 body.on('error', error_ => {
84 const error = error_ instanceof FetchBaseError ?
85 error_ :
86 new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, 'system', error_);
87 this[INTERNALS].error = error;
88 });
89 }
90 }
91
92 get body() {
93 return this[INTERNALS].stream;
94 }
95
96 get bodyUsed() {
97 return this[INTERNALS].disturbed;
98 }
99
100 /**
101 * Decode response as ArrayBuffer
102 *
103 * @return Promise
104 */
105 async arrayBuffer() {
106 const {buffer, byteOffset, byteLength} = await consumeBody(this);
107 return buffer.slice(byteOffset, byteOffset + byteLength);
108 }
109
110 async formData() {
111 const ct = this.headers.get('content-type');
112
113 if (ct.startsWith('application/x-www-form-urlencoded')) {
114 const formData = new FormData();
115 const parameters = new URLSearchParams(await this.text());
116
117 for (const [name, value] of parameters) {
118 formData.append(name, value);
119 }
120
121 return formData;
122 }
123
124 const {toFormData} = await import('./utils/multipart-parser.js');
125 return toFormData(this.body, ct);
126 }
127
128 /**
129 * Return raw response as Blob
130 *
131 * @return Promise
132 */
133 async blob() {
134 const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || '';
135 const buf = await this.arrayBuffer();
136
137 return new Blob([buf], {
138 type: ct
139 });
140 }
141
142 /**
143 * Decode response as json
144 *
145 * @return Promise
146 */
147 async json() {
148 const text = await this.text();
149 return JSON.parse(text);
150 }
151
152 /**
153 * Decode response as text
154 *
155 * @return Promise
156 */
157 async text() {
158 const buffer = await consumeBody(this);
159 return new TextDecoder().decode(buffer);
160 }
161
162 /**
163 * Decode response as buffer (non-spec api)
164 *
165 * @return Promise
166 */
167 buffer() {
168 return consumeBody(this);
169 }
170}
171
172Body.prototype.buffer = deprecate(Body.prototype.buffer, 'Please use \'response.arrayBuffer()\' instead of \'response.buffer()\'', 'node-fetch#buffer');
173
174// In browsers, all properties are enumerable.
175Object.defineProperties(Body.prototype, {
176 body: {enumerable: true},
177 bodyUsed: {enumerable: true},
178 arrayBuffer: {enumerable: true},
179 blob: {enumerable: true},
180 json: {enumerable: true},
181 text: {enumerable: true},
182 data: {get: deprecate(() => {},
183 'data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead',
184 'https://github.com/node-fetch/node-fetch/issues/1000 (response)')}
185});
186
187/**
188 * Consume and convert an entire Body to a Buffer.
189 *
190 * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
191 *
192 * @return Promise
193 */
194async function consumeBody(data) {
195 if (data[INTERNALS].disturbed) {
196 throw new TypeError(`body used already for: ${data.url}`);
197 }
198
199 data[INTERNALS].disturbed = true;
200
201 if (data[INTERNALS].error) {
202 throw data[INTERNALS].error;
203 }
204
205 const {body} = data;
206
207 // Body is null
208 if (body === null) {
209 return Buffer.alloc(0);
210 }
211
212 /* c8 ignore next 3 */
213 if (!(body instanceof Stream)) {
214 return Buffer.alloc(0);
215 }
216
217 // Body is stream
218 // get ready to actually consume the body
219 const accum = [];
220 let accumBytes = 0;
221
222 try {
223 for await (const chunk of body) {
224 if (data.size > 0 && accumBytes + chunk.length > data.size) {
225 const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
226 body.destroy(error);
227 throw error;
228 }
229
230 accumBytes += chunk.length;
231 accum.push(chunk);
232 }
233 } catch (error) {
234 const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error);
235 throw error_;
236 }
237
238 if (body.readableEnded === true || body._readableState.ended === true) {
239 try {
240 if (accum.every(c => typeof c === 'string')) {
241 return Buffer.from(accum.join(''));
242 }
243
244 return Buffer.concat(accum, accumBytes);
245 } catch (error) {
246 throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
247 }
248 } else {
249 throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`);
250 }
251}
252
253/**
254 * Clone body given Res/Req instance
255 *
256 * @param Mixed instance Response or Request instance
257 * @param String highWaterMark highWaterMark for both PassThrough body streams
258 * @return Mixed
259 */
260export const clone = (instance, highWaterMark) => {
261 let p1;
262 let p2;
263 let {body} = instance[INTERNALS];
264
265 // Don't allow cloning a used body
266 if (instance.bodyUsed) {
267 throw new Error('cannot clone body after it is used');
268 }
269
270 // Check that body is a stream and not form-data object
271 // note: we can't clone the form-data object without having it as a dependency
272 if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
273 // Tee instance body
274 p1 = new PassThrough({highWaterMark});
275 p2 = new PassThrough({highWaterMark});
276 body.pipe(p1);
277 body.pipe(p2);
278 // Set instance body to teed body and return the other teed body
279 instance[INTERNALS].stream = p1;
280 body = p2;
281 }
282
283 return body;
284};
285
286const getNonSpecFormDataBoundary = deprecate(
287 body => body.getBoundary(),
288 'form-data doesn\'t follow the spec and requires special treatment. Use alternative package',
289 'https://github.com/node-fetch/node-fetch/issues/1167'
290);
291
292/**
293 * Performs the operation "extract a `Content-Type` value from |object|" as
294 * specified in the specification:
295 * https://fetch.spec.whatwg.org/#concept-bodyinit-extract
296 *
297 * This function assumes that instance.body is present.
298 *
299 * @param {any} body Any options.body input
300 * @returns {string | null}
301 */
302export const extractContentType = (body, request) => {
303 // Body is null or undefined
304 if (body === null) {
305 return null;
306 }
307
308 // Body is string
309 if (typeof body === 'string') {
310 return 'text/plain;charset=UTF-8';
311 }
312
313 // Body is a URLSearchParams
314 if (isURLSearchParameters(body)) {
315 return 'application/x-www-form-urlencoded;charset=UTF-8';
316 }
317
318 // Body is blob
319 if (isBlob(body)) {
320 return body.type || null;
321 }
322
323 // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView)
324 if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
325 return null;
326 }
327
328 if (body instanceof FormData) {
329 return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
330 }
331
332 // Detect form data input from form-data module
333 if (body && typeof body.getBoundary === 'function') {
334 return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`;
335 }
336
337 // Body is stream - can't really do much about this
338 if (body instanceof Stream) {
339 return null;
340 }
341
342 // Body constructor defaults other things to string
343 return 'text/plain;charset=UTF-8';
344};
345
346/**
347 * The Fetch Standard treats this as if "total bytes" is a property on the body.
348 * For us, we have to explicitly get it with a function.
349 *
350 * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
351 *
352 * @param {any} obj.body Body object from the Body instance.
353 * @returns {number | null}
354 */
355export const getTotalBytes = request => {
356 const {body} = request[INTERNALS];
357
358 // Body is null or undefined
359 if (body === null) {
360 return 0;
361 }
362
363 // Body is Blob
364 if (isBlob(body)) {
365 return body.size;
366 }
367
368 // Body is Buffer
369 if (Buffer.isBuffer(body)) {
370 return body.length;
371 }
372
373 // Detect form data input from form-data module
374 if (body && typeof body.getLengthSync === 'function') {
375 return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
376 }
377
378 // Body is stream
379 return null;
380};
381
382/**
383 * Write a Body to a Node.js WritableStream (e.g. http.Request) object.
384 *
385 * @param {Stream.Writable} dest The stream to write to.
386 * @param obj.body Body object from the Body instance.
387 * @returns {Promise<void>}
388 */
389export const writeToStream = async (dest, {body}) => {
390 if (body === null) {
391 // Body is null
392 dest.end();
393 } else {
394 // Body is stream
395 await pipeline(body, dest);
396 }
397};