1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import Stream, {PassThrough} from 'node:stream';
|
9 | import {types, deprecate, promisify} from 'node:util';
|
10 | import {Buffer} from 'node:buffer';
|
11 |
|
12 | import Blob from 'fetch-blob';
|
13 | import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js';
|
14 |
|
15 | import {FetchError} from './errors/fetch-error.js';
|
16 | import {FetchBaseError} from './errors/base.js';
|
17 | import {isBlob, isURLSearchParameters} from './utils/is.js';
|
18 |
|
19 | const pipeline = promisify(Stream.pipeline);
|
20 | const INTERNALS = Symbol('Body internals');
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 | export default class Body {
|
32 | constructor(body, {
|
33 | size = 0
|
34 | } = {}) {
|
35 | let boundary = null;
|
36 |
|
37 | if (body === null) {
|
38 |
|
39 | body = null;
|
40 | } else if (isURLSearchParameters(body)) {
|
41 |
|
42 | body = Buffer.from(body.toString());
|
43 | } else if (isBlob(body)) {
|
44 |
|
45 | } else if (Buffer.isBuffer(body)) {
|
46 |
|
47 | } else if (types.isAnyArrayBuffer(body)) {
|
48 |
|
49 | body = Buffer.from(body);
|
50 | } else if (ArrayBuffer.isView(body)) {
|
51 |
|
52 | body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
53 | } else if (body instanceof Stream) {
|
54 |
|
55 | } else if (body instanceof FormData) {
|
56 |
|
57 | body = formDataToBlob(body);
|
58 | boundary = body.type.split('=')[1];
|
59 | } else {
|
60 |
|
61 |
|
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 |
|
102 |
|
103 |
|
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 |
|
130 |
|
131 |
|
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 |
|
144 |
|
145 |
|
146 |
|
147 | async json() {
|
148 | const text = await this.text();
|
149 | return JSON.parse(text);
|
150 | }
|
151 |
|
152 | |
153 |
|
154 |
|
155 |
|
156 |
|
157 | async text() {
|
158 | const buffer = await consumeBody(this);
|
159 | return new TextDecoder().decode(buffer);
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 |
|
167 | buffer() {
|
168 | return consumeBody(this);
|
169 | }
|
170 | }
|
171 |
|
172 | Body.prototype.buffer = deprecate(Body.prototype.buffer, 'Please use \'response.arrayBuffer()\' instead of \'response.buffer()\'', 'node-fetch#buffer');
|
173 |
|
174 |
|
175 | Object.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 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | async 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 |
|
208 | if (body === null) {
|
209 | return Buffer.alloc(0);
|
210 | }
|
211 |
|
212 |
|
213 | if (!(body instanceof Stream)) {
|
214 | return Buffer.alloc(0);
|
215 | }
|
216 |
|
217 |
|
218 |
|
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 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 | export const clone = (instance, highWaterMark) => {
|
261 | let p1;
|
262 | let p2;
|
263 | let {body} = instance[INTERNALS];
|
264 |
|
265 |
|
266 | if (instance.bodyUsed) {
|
267 | throw new Error('cannot clone body after it is used');
|
268 | }
|
269 |
|
270 |
|
271 |
|
272 | if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
|
273 |
|
274 | p1 = new PassThrough({highWaterMark});
|
275 | p2 = new PassThrough({highWaterMark});
|
276 | body.pipe(p1);
|
277 | body.pipe(p2);
|
278 |
|
279 | instance[INTERNALS].stream = p1;
|
280 | body = p2;
|
281 | }
|
282 |
|
283 | return body;
|
284 | };
|
285 |
|
286 | const 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 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 | export const extractContentType = (body, request) => {
|
303 |
|
304 | if (body === null) {
|
305 | return null;
|
306 | }
|
307 |
|
308 |
|
309 | if (typeof body === 'string') {
|
310 | return 'text/plain;charset=UTF-8';
|
311 | }
|
312 |
|
313 |
|
314 | if (isURLSearchParameters(body)) {
|
315 | return 'application/x-www-form-urlencoded;charset=UTF-8';
|
316 | }
|
317 |
|
318 |
|
319 | if (isBlob(body)) {
|
320 | return body.type || null;
|
321 | }
|
322 |
|
323 |
|
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 |
|
333 | if (body && typeof body.getBoundary === 'function') {
|
334 | return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`;
|
335 | }
|
336 |
|
337 |
|
338 | if (body instanceof Stream) {
|
339 | return null;
|
340 | }
|
341 |
|
342 |
|
343 | return 'text/plain;charset=UTF-8';
|
344 | };
|
345 |
|
346 |
|
347 |
|
348 |
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 | export const getTotalBytes = request => {
|
356 | const {body} = request[INTERNALS];
|
357 |
|
358 |
|
359 | if (body === null) {
|
360 | return 0;
|
361 | }
|
362 |
|
363 |
|
364 | if (isBlob(body)) {
|
365 | return body.size;
|
366 | }
|
367 |
|
368 |
|
369 | if (Buffer.isBuffer(body)) {
|
370 | return body.length;
|
371 | }
|
372 |
|
373 |
|
374 | if (body && typeof body.getLengthSync === 'function') {
|
375 | return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
|
376 | }
|
377 |
|
378 |
|
379 | return null;
|
380 | };
|
381 |
|
382 |
|
383 |
|
384 |
|
385 |
|
386 |
|
387 |
|
388 |
|
389 | export const writeToStream = async (dest, {body}) => {
|
390 | if (body === null) {
|
391 |
|
392 | dest.end();
|
393 | } else {
|
394 |
|
395 | await pipeline(body, dest);
|
396 | }
|
397 | };
|