Skip to content

Commit

Permalink
Do not patch global Headers if it is native, and support URL as a fir…
Browse files Browse the repository at this point in the history
…st parameter of `fetch`
  • Loading branch information
ardatan committed Oct 2, 2022
1 parent 04833c7 commit e59cbb6
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 147 deletions.
6 changes: 6 additions & 0 deletions .changeset/five-donkeys-behave.md
@@ -0,0 +1,6 @@
---
'@whatwg-node/fetch': patch
'@whatwg-node/server': patch
---

Do not patch global Headers if it is native, and support URL as a first parameter of `fetch`
42 changes: 21 additions & 21 deletions packages/fetch/dist/create-node-ponyfill.js
Expand Up @@ -11,7 +11,7 @@ module.exports = function createNodePonyfill(opts = {}) {
const ponyfills = {};

if (!opts.useNodeFetch) {
ponyfills.fetch = globalThis.fetch; // To enable: import {fetch} from 'cross-fetch'
ponyfills.fetch = globalThis.fetch;
ponyfills.Headers = globalThis.Headers;
ponyfills.Request = globalThis.Request;
ponyfills.Response = globalThis.Response;
Expand Down Expand Up @@ -121,8 +121,7 @@ module.exports = function createNodePonyfill(opts = {}) {

class Request extends OriginalRequest {
constructor(requestOrUrl, options) {
if (typeof requestOrUrl === "string") {
options = options || {};
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) {
super(requestOrUrl, options);
const contentType = this.headers.get("content-type");
if (contentType && contentType.startsWith("multipart/form-data")) {
Expand All @@ -140,7 +139,7 @@ module.exports = function createNodePonyfill(opts = {}) {
const originalFetch = ponyfills.fetch || undici.fetch;

const fetch = function (requestOrUrl, options) {
if (typeof requestOrUrl === "string") {
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) {
// We cannot use our ctor because it leaks on Node 18's global fetch
return originalFetch(requestOrUrl, options);
}
Expand Down Expand Up @@ -169,7 +168,7 @@ module.exports = function createNodePonyfill(opts = {}) {
if (!ponyfills.Headers) {
ponyfills.Headers = nodeFetch.Headers;
// Sveltekit
if (globalThis.Headers) {
if (globalThis.Headers && nodeMajor < 18) {
Object.defineProperty(globalThis.Headers, Symbol.hasInstance, {
value(obj) {
return obj && obj.get && obj.set && obj.delete && obj.has && obj.append;
Expand All @@ -192,28 +191,29 @@ module.exports = function createNodePonyfill(opts = {}) {

class Request extends OriginalRequest {
constructor(requestOrUrl, options) {
if (typeof requestOrUrl === "string") {
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) {
// Support schemaless URIs on the server for parity with the browser.
// Ex: //github.com/ -> https://github.com/
if (/^\/\//.test(requestOrUrl)) {
requestOrUrl = "https:" + requestOrUrl;
if (/^\/\//.test(requestOrUrl.toString())) {
requestOrUrl = "https:" + requestOrUrl.toString();
}
options = options || {};
options.headers = new ponyfills.Headers(options.headers || {});
options.headers.set('Connection', 'keep-alive');
if (options.body != null) {
if (options.body[Symbol.toStringTag] === 'FormData') {
const encoder = new formDataEncoderModule.FormDataEncoder(options.body)
const fixedOptions = {
...options
};
fixedOptions.headers = new ponyfills.Headers(fixedOptions.headers || {});
fixedOptions.headers.set('Connection', 'keep-alive');
if (fixedOptions.body != null) {
if (fixedOptions.body[Symbol.toStringTag] === 'FormData') {
const encoder = new formDataEncoderModule.FormDataEncoder(fixedOptions.body)
for (const headerKey in encoder.headers) {
options.headers.set(headerKey, encoder.headers[headerKey])
fixedOptions.headers.set(headerKey, encoder.headers[headerKey])
}
options.body = streams.Readable.from(encoder.encode());
}
if (options.body[Symbol.toStringTag] === 'ReadableStream') {
options.body = readableStreamToReadable(options.body);
fixedOptions.body = streams.Readable.from(encoder);
} else if (fixedOptions.body[Symbol.toStringTag] === 'ReadableStream') {
fixedOptions.body = readableStreamToReadable(fixedOptions.body);
}
}
super(requestOrUrl, options);
super(requestOrUrl, fixedOptions);
} else {
super(requestOrUrl);
}
Expand All @@ -222,7 +222,7 @@ module.exports = function createNodePonyfill(opts = {}) {
}
ponyfills.Request = Request;
const fetch = function (requestOrUrl, options) {
if (typeof requestOrUrl === "string") {
if (typeof requestOrUrl === "string" || requestOrUrl instanceof URL) {
return fetch(new Request(requestOrUrl, options));
}
if (requestOrUrl.url.startsWith('file:')) {
Expand Down
31 changes: 16 additions & 15 deletions packages/fetch/dist/readableStreamToReadable.js
@@ -1,19 +1,20 @@
const streams = require('stream');

module.exports = function readableStreamToReadable(readableStream) {
return streams.Readable.from({
[Symbol.asyncIterator]() {
const reader = readableStream.getReader();
return {
next() {
return reader.read();
},
async return() {
reader.releaseLock();
await readableStream.cancel();
return Promise.resolve({ done: true });
}
}
}
});
return streams.Readable.from({
[Symbol.asyncIterator]() {
const reader = readableStream.getReader();
return {
next() {
return reader.read();
},
async return() {
reader.cancel();
reader.releaseLock();
await readableStream.cancel();
return { done: true };
}
}
}
});
}
76 changes: 40 additions & 36 deletions packages/fetch/tests/getFormDataMethod.spec.ts
@@ -1,43 +1,47 @@
import { createFetch } from '@whatwg-node/fetch';
import { createTestContainer } from '../../server/test/create-test-container';

describe('getFormDataMethod', () => {
['fieldsFirst:true', 'fieldsFirst:false'].forEach(fieldsFirstFlag => {
const fetchAPI = createFetch({
formDataLimits: {
fieldsFirst: fieldsFirstFlag === 'fieldsFirst:true',
},
});
describe(fieldsFirstFlag, () => {
it('should parse fields correctly', async () => {
const formData = new fetchAPI.FormData();
formData.append('greetings', 'Hello world!');
formData.append('bye', 'Goodbye world!');
const request = new fetchAPI.Request('http://localhost:8080', {
method: 'POST',
body: formData,
});
const formdata = await request.formData();
expect(formdata.get('greetings')).toBe('Hello world!');
expect(formdata.get('bye')).toBe('Goodbye world!');
});
it('should parse and receive text files correctly', async () => {
const formData = new fetchAPI.FormData();
const greetingsFile = new fetchAPI.File(['Hello world!'], 'greetings.txt', { type: 'text/plain' });
const byeFile = new fetchAPI.File(['Goodbye world!'], 'bye.txt', { type: 'text/plain' });
formData.append('greetings', greetingsFile);
formData.append('bye', byeFile);
const request = new fetchAPI.Request('http://localhost:8080', {
method: 'POST',
body: formData,
});
const formdata = await request.formData();
const receivedGreetingsFile = formdata.get('greetings') as File;
const receivedGreetingsText = await receivedGreetingsFile.text();
expect(receivedGreetingsText).toBe('Hello world!');
const receivedByeFile = formdata.get('bye') as File;
const receivedByeText = await receivedByeFile.text();
expect(receivedByeText).toBe('Goodbye world!');
});
createTestContainer(
fetchAPI => {
it('should parse fields correctly', async () => {
const formData = new fetchAPI.FormData();
formData.append('greetings', 'Hello world!');
formData.append('bye', 'Goodbye world!');
const request = new fetchAPI.Request('http://localhost:8080', {
method: 'POST',
body: formData,
});
const formdata = await request.formData();
expect(formdata.get('greetings')).toBe('Hello world!');
expect(formdata.get('bye')).toBe('Goodbye world!');
});
it('should parse and receive text files correctly', async () => {
const formData = new fetchAPI.FormData();
const greetingsFile = new fetchAPI.File(['Hello world!'], 'greetings.txt', { type: 'text/plain' });
const byeFile = new fetchAPI.File(['Goodbye world!'], 'bye.txt', { type: 'text/plain' });
formData.append('greetings', greetingsFile);
formData.append('bye', byeFile);
const request = new fetchAPI.Request('http://localhost:8080', {
method: 'POST',
body: formData,
});
const formdata = await request.formData();
const receivedGreetingsFile = formdata.get('greetings') as File;
const receivedGreetingsText = await receivedGreetingsFile.text();
expect(receivedGreetingsText).toBe('Hello world!');
const receivedByeFile = formdata.get('bye') as File;
const receivedByeText = await receivedByeFile.text();
expect(receivedByeText).toBe('Goodbye world!');
});
},
{
formDataLimits: {
fieldsFirst: fieldsFirstFlag === 'fieldsFirst:true',
},
}
);
});
});
});
6 changes: 3 additions & 3 deletions packages/server/src/index.ts
Expand Up @@ -185,9 +185,9 @@ function createServerAdapter<
if (typeof input === 'string' || input instanceof URL) {
const [initOrCtx, ...restOfCtx] = maybeCtx;
if (isRequestInit(initOrCtx)) {
return handleRequestWithWaitUntil(new RequestCtor(input.toString(), initOrCtx), ...restOfCtx);
return handleRequestWithWaitUntil(new RequestCtor(input, initOrCtx), ...restOfCtx);
}
return handleRequestWithWaitUntil(new RequestCtor(input.toString()), ...maybeCtx);
return handleRequestWithWaitUntil(new RequestCtor(input), ...maybeCtx);
}
return handleRequestWithWaitUntil(input, ...maybeCtx);
};
Expand All @@ -206,7 +206,7 @@ function createServerAdapter<
}

if (isServerResponse(initOrCtxOrRes)) {
throw new Error('Got Node response without Node request');
throw new TypeError('Got Node response without Node request');
}

// Is input a container object over Request?
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/utils.ts
Expand Up @@ -177,8 +177,8 @@ export async function sendNodeResponse(
serverResponse.end(body, resolve);
} else if (isReadable(body)) {
serverResponse.once('close', () => {
resolve();
body.destroy();
resolve();
});
body.pipe(serverResponse);
} else if (isAsyncIterable(body)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/test/create-test-container.ts
Expand Up @@ -4,7 +4,7 @@ export function createTestContainer(
fn: (fetchAPI: ReturnType<typeof createFetch>) => void,
extraFlags: Parameters<typeof createFetch>[0] = {}
) {
['default-fetch' /*, 'node-fetch' */].forEach(fetchImplementation => {
['default-fetch', 'node-fetch'].forEach(fetchImplementation => {
describe(fetchImplementation, () => {
const fetchAPI = createFetch({
useNodeFetch: fetchImplementation === 'node-fetch',
Expand Down

0 comments on commit e59cbb6

Please sign in to comment.