Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

diff #1

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
824 changes: 42 additions & 782 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-fetch",
"version": "3.0.0-beta.9",
"name": "@web-std/node-fetch",
"version": "1.0.0",
"description": "A light-weight module that brings Fetch API to node.js",
"main": "./dist/index.cjs",
"module": "./src/index.js",
Expand Down
44 changes: 25 additions & 19 deletions src/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,26 @@ import {FetchError} from './errors/fetch-error.js';
import {FetchBaseError} from './errors/base.js';
import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js';
import {isBlob, isURLSearchParameters, isFormData} from './utils/is.js';
import {blobToNodeStream} from './utils/blob-to-stream.js';

const INTERNALS = Symbol('Body internals');
export const BODY = Symbol('Body content');

/**
* Body mixin
*
* Ref: https://fetch.spec.whatwg.org/#body
*
* @param Stream body Readable stream
* @param Stream stream Readable stream
* @param Object opts Response options
* @return Void
*/
export default class Body {
/**
*
* @param {BodyInit|Stream} body
* @param {{size?:number}} options
*/
constructor(body, {
size = 0
} = {}) {
Expand Down Expand Up @@ -61,6 +68,7 @@ export default class Body {
}

this[INTERNALS] = {
/** @type {Stream|Buffer|Blob|null} */
body,
boundary,
disturbed: false,
Expand All @@ -78,8 +86,8 @@ export default class Body {
}
}

get body() {
return this[INTERNALS].body;
get [BODY]() {
return this[INTERNALS].body
}

get bodyUsed() {
Expand Down Expand Up @@ -155,7 +163,8 @@ Object.defineProperties(Body.prototype, {
*
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
*
* @return Promise
* @param {Body} data
* @return {Promise<Buffer>}
*/
async function consumeBody(data) {
if (data[INTERNALS].disturbed) {
Expand All @@ -168,7 +177,7 @@ async function consumeBody(data) {
throw data[INTERNALS].error;
}

let {body} = data;
let {body} = data[INTERNALS];

// Body is null
if (body === null) {
Expand All @@ -177,7 +186,7 @@ async function consumeBody(data) {

// Body is blob
if (isBlob(body)) {
body = body.stream();
body = blobToNodeStream(body);
}

// Body is buffer
Expand All @@ -197,14 +206,15 @@ async function consumeBody(data) {

try {
for await (const chunk of body) {
if (data.size > 0 && accumBytes + chunk.length > data.size) {
const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
if (data.size > 0 && accumBytes + bytes.byteLength > data.size) {
const err = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
body.destroy(err);
throw err;
}

accumBytes += chunk.length;
accum.push(chunk);
accumBytes += bytes.byteLength;
accum.push(bytes);
}
} catch (error) {
if (error instanceof FetchBaseError) {
Expand All @@ -217,10 +227,6 @@ async function consumeBody(data) {

if (body.readableEnded === true || body._readableState.ended === true) {
try {
if (accum.every(c => typeof c === 'string')) {
return Buffer.from(accum.join(''));
}

return Buffer.concat(accum, accumBytes);
} catch (error) {
throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error);
Expand All @@ -240,7 +246,7 @@ async function consumeBody(data) {
export const clone = (instance, highWaterMark) => {
let p1;
let p2;
let {body} = instance;
let {body} = instance[INTERNALS];

// Don't allow cloning a used body
if (instance.bodyUsed) {
Expand Down Expand Up @@ -323,11 +329,11 @@ export const extractContentType = (body, request) => {
*
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
*
* @param {any} obj.body Body object from the Body instance.
* @param {Body} request Body object from the Body instance.
* @returns {number | null}
*/
export const getTotalBytes = request => {
const {body} = request;
const body = request[BODY];

// Body is null or undefined
if (body === null) {
Expand Down Expand Up @@ -362,16 +368,16 @@ export const getTotalBytes = request => {
* Write a Body to a Node.js WritableStream (e.g. http.Request) object.
*
* @param {Stream.Writable} dest The stream to write to.
* @param obj.body Body object from the Body instance.
* @param {null|Buffer|Blob|Stream} body Body object from the Body instance.
* @returns {void}
*/
export const writeToStream = (dest, {body}) => {
export const writeToStream = (dest, body) => {
if (body === null) {
// Body is null
dest.end();
} else if (isBlob(body)) {
// Body is Blob
body.stream().pipe(dest);
blobToNodeStream(body).pipe(dest);
} else if (Buffer.isBuffer(body)) {
// Body is buffer
dest.write(body);
Expand Down
20 changes: 10 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import zlib from 'zlib';
import Stream, {PassThrough, pipeline as pump} from 'stream';
import dataUriToBuffer from 'data-uri-to-buffer';

import {writeToStream} from './body.js';
import {writeToStream, BODY} from './body.js';
import Response from './response.js';
import Headers, {fromRawHeaders} from './headers.js';
import Request, {getNodeRequestOptions} from './request.js';
Expand Down Expand Up @@ -55,15 +55,15 @@ export default async function fetch(url, options_) {
const abort = () => {
const error = new AbortError('The operation was aborted.');
reject(error);
if (request.body && request.body instanceof Stream.Readable) {
request.body.destroy(error);
if (request[BODY] && request[BODY] instanceof Stream.Readable) {
request[BODY].destroy(error);
}

if (!response || !response.body) {
if (!response || !response[BODY]) {
return;
}

response.body.emit('error', error);
response[BODY].emit('error', error);
};

if (signal && signal.aborted) {
Expand Down Expand Up @@ -96,7 +96,7 @@ export default async function fetch(url, options_) {
});

fixResponseChunkedTransferBadEnding(request_, err => {
response.body.destroy(err);
response[BODY].destroy(err);
});

/* c8 ignore next 18 */
Expand All @@ -113,7 +113,7 @@ export default async function fetch(url, options_) {
if (response && endedWithEventsCount < s._eventsCount && !hadError) {
const err = new Error('Premature close');
err.code = 'ERR_STREAM_PREMATURE_CLOSE';
response.body.emit('error', err);
response[BODY].emit('error', err);
}
});
});
Expand Down Expand Up @@ -166,13 +166,13 @@ export default async function fetch(url, options_) {
agent: request.agent,
compress: request.compress,
method: request.method,
body: request.body,
body: request[BODY],
signal: request.signal,
size: request.size
};

// HTTP-redirect fetch step 9
if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) {
if (response_.statusCode !== 303 && request[BODY] && options_.body instanceof Stream.Readable) {
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
finalize();
return;
Expand Down Expand Up @@ -286,7 +286,7 @@ export default async function fetch(url, options_) {
resolve(response);
});

writeToStream(request_, request);
writeToStream(request_, request[BODY]);
});
}

Expand Down
20 changes: 12 additions & 8 deletions src/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import {format as formatUrl} from 'url';
import Headers from './headers.js';
import Body, {clone, extractContentType, getTotalBytes} from './body.js';
import Body, {clone, extractContentType, getTotalBytes, BODY} from './body.js';
import {isAbortSignal} from './utils/is.js';
import {getSearch} from './utils/get-search.js';

Expand All @@ -18,8 +18,8 @@ const INTERNALS = Symbol('Request internals');
/**
* Check if `obj` is an instance of Request.
*
* @param {*} obj
* @return {boolean}
* @param {any} object
* @return {object is Request}
*/
const isRequest = object => {
return (
Expand All @@ -36,6 +36,10 @@ const isRequest = object => {
* @return Void
*/
export default class Request extends Body {
/**
* @param {RequestInfo} input Url or Request instance
* @param {RequestInit} init Custom options
*/
constructor(input, init = {}) {
let parsedURL;

Expand All @@ -51,14 +55,14 @@ export default class Request extends Body {
method = method.toUpperCase();

// eslint-disable-next-line no-eq-null, eqeqeq
if (((init.body != null || isRequest(input)) && input.body !== null) &&
if (((isRequest(input) || init.body != null) && input[BODY] !== null) &&
(method === 'GET' || method === 'HEAD')) {
throw new TypeError('Request with GET/HEAD method cannot have body');
}

const inputBody = init.body ?
init.body :
(isRequest(input) && input.body !== null ?
(isRequest(input) && input[BODY] !== null ?
clone(input) :
null);

Expand Down Expand Up @@ -150,7 +154,7 @@ Object.defineProperties(Request.prototype, {
/**
* Convert a Request to Node.js http request options.
*
* @param Request A Request instance
* @param {Request} A Request instance
* @return Object The options object to be passed to http.request
*/
export const getNodeRequestOptions = request => {
Expand All @@ -164,11 +168,11 @@ export const getNodeRequestOptions = request => {

// HTTP-network-or-cache fetch steps 2.4-2.7
let contentLengthValue = null;
if (request.body === null && /^(post|put)$/i.test(request.method)) {
if (request[BODY] === null && /^(post|put)$/i.test(request.method)) {
contentLengthValue = '0';
}

if (request.body !== null) {
if (request[BODY] !== null) {
const totalBytes = getTotalBytes(request);
// Set Content-Length if totalBytes is a number (that is not NaN)
if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
Expand Down
2 changes: 1 addition & 1 deletion src/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default class Response extends Body {
const headers = new Headers(options.headers);

if (body !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(body);
const contentType = extractContentType(body, this);
if (contentType) {
headers.append('Content-Type', contentType);
}
Expand Down
23 changes: 23 additions & 0 deletions src/utils/blob-to-stream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Readable} from 'stream';

// 64 KiB (same size chrome slice theirs blob into Uint8array's)
const POOL_SIZE = 65536;

/* c8 ignore start */
async function * read(blob) {
let position = 0;
while (position !== blob.size) {
const chunk = blob.slice(position, Math.min(blob.size, position + POOL_SIZE));
// eslint-disable-next-line no-await-in-loop
const buffer = await chunk.arrayBuffer();
position += buffer.byteLength;
yield new Uint8Array(buffer);
}
}
/* c8 ignore end */

export function blobToNodeStream(blob) {
return Readable.from(blob.stream ? blob.stream() : read(blob), {
objectMode: false
});
}
16 changes: 10 additions & 6 deletions src/utils/is.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const NAME = Symbol.toStringTag;
* ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
*
* @param {*} obj
* @return {boolean}
* @return {object is URLSearchParams}
*/
export const isURLSearchParameters = object => {
return (
Expand All @@ -31,24 +31,28 @@ export const isURLSearchParameters = object => {
* Check if `object` is a W3C `Blob` object (which `File` inherits from)
*
* @param {*} obj
* @return {boolean}
* @return {object is Blob}
*/
export const isBlob = object => {
return (
typeof object === 'object' &&
typeof object.arrayBuffer === 'function' &&
typeof object.type === 'string' &&
typeof object.stream === 'function' &&
// typeof object.stream === 'function' &&
typeof object.constructor === 'function' &&
/^(Blob|File)$/.test(object[NAME])
(
/* c8 ignore next 2 */
/^(Blob|File)$/.test(object[NAME]) ||
/^(Blob|File)$/.test(object.constructor.name)
)
);
};

/**
* Check if `obj` is a spec-compliant `FormData` object
*
* @param {*} object
* @return {boolean}
* @return {object is FormData}
*/
export function isFormData(object) {
return (
Expand All @@ -70,7 +74,7 @@ export function isFormData(object) {
* Check if `obj` is an instance of AbortSignal.
*
* @param {*} obj
* @return {boolean}
* @return {object is AbortSignal}
*/
export const isAbortSignal = object => {
return (
Expand Down
Loading