Skip to content

Commit

Permalink
Fixes per WPT tests (#1219)
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan authored Mar 22, 2024
1 parent ac6b719 commit fa097a4
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-coats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@whatwg-node/node-fetch": patch
---

Support blob: object URLs
5 changes: 5 additions & 0 deletions .changeset/lazy-cats-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@whatwg-node/node-fetch": patch
---

Throw TypeError when multipart request is unable to parse as FormData
5 changes: 3 additions & 2 deletions packages/node-fetch/src/Body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,9 @@ export class PonyfillBody<TJSON = any> implements Body {
bb.on('close', () => {
resolve(formData);
});
bb.on('error', err => {
reject(err);
bb.on('error', (err: any = 'An error occurred while parsing the form data') => {
const errMessage = err.message || err.toString();
reject(new TypeError(errMessage, err.cause));
});
_body?.readable.pipe(bb);
});
Expand Down
23 changes: 23 additions & 0 deletions packages/node-fetch/src/URL.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { resolveObjectURL } from 'buffer';
import { randomUUID } from 'crypto';
import FastQuerystring from 'fast-querystring';
import FastUrl from '@kamilkisiela/fast-url-parser';
import { PonyfillBlob } from './Blob.js';
import { PonyfillURLSearchParams } from './URLSearchParams.js';

FastUrl.queryString = FastQuerystring;
Expand Down Expand Up @@ -57,4 +60,24 @@ export class PonyfillURL extends FastUrl implements URL {
toJSON(): string {
return this.toString();
}

private static blobRegistry = new Map<string, Blob>();

static createObjectURL(blob: Blob): string {
const blobUrl = `blob:whatwgnode:${randomUUID()}`;
this.blobRegistry.set(blobUrl, blob);
return blobUrl;
}

static resolveObjectURL(url: string): void {
if (!this.blobRegistry.has(url)) {
URL.revokeObjectURL(url);
} else {
this.blobRegistry.delete(url);
}
}

static getBlobFromURL(url: string): Blob | PonyfillBlob | undefined {
return (this.blobRegistry.get(url) || resolveObjectURL(url)) as Blob | PonyfillBlob | undefined;
}
}
19 changes: 19 additions & 0 deletions packages/node-fetch/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fetchCurl } from './fetchCurl.js';
import { fetchNodeHttp } from './fetchNodeHttp.js';
import { PonyfillRequest, RequestPonyfillInit } from './Request.js';
import { PonyfillResponse } from './Response.js';
import { PonyfillURL } from './URL.js';
import { fakePromise } from './utils.js';

const BASE64_SUFFIX = ';base64';
Expand Down Expand Up @@ -37,6 +38,20 @@ function getResponseForDataUri(url: string) {
});
}

function getResponseForBlob(url: string) {
const blob = PonyfillURL.getBlobFromURL(url);
if (!blob) {
throw new TypeError('Invalid Blob URL');
}
return new PonyfillResponse(blob, {
status: 200,
headers: {
'content-type': blob.type,
'content-length': blob.size.toString(),
},
});
}

function isURL(obj: any): obj is URL {
return obj != null && obj.href != null;
}
Expand All @@ -59,6 +74,10 @@ export function fetchPonyfill<TResponseJSON = any, TRequestJSON = any>(
const response = getResponseForFile(fetchRequest.url);
return fakePromise(response);
}
if (fetchRequest.url.startsWith('blob:')) {
const response = getResponseForBlob(fetchRequest.url);
return fakePromise(response);
}
if (globalThis.libcurl) {
return fetchCurl(fetchRequest);
}
Expand Down
17 changes: 17 additions & 0 deletions packages/node-fetch/tests/Body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,21 @@ describe('Body', () => {
const result = decoder.decode(buf);
expect(result).toBe('hello world');
});

it('throws a TypeError if the body is unable to parse as FormData', async () => {
const formStr =
'--Boundary_with_capital_letters\r\n' +
'Content-Type: application/json\r\n' +
'Content-Disposition: form-data; name="does_this_work"\r\n' +
'\r\n' +
'YES\r\n' +
'--Boundary_with_capital_letters-Random junk';

const body = new PonyfillBody(
new PonyfillBlob([formStr], {
type: 'multipart/form-data; boundary=Boundary_with_capital_letters',
}),
);
await expect(() => body.formData()).rejects.toThrow(TypeError);
});
});
45 changes: 45 additions & 0 deletions packages/node-fetch/tests/fetch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Blob as NodeBlob } from 'buffer';
import { Readable } from 'stream';
import { URL as NodeURL } from 'url';
import { runTestsForEachFetchImpl } from '../../server/test/test-fetch.js';
import { PonyfillBlob } from '../src/Blob.js';
import { fetchPonyfill } from '../src/fetch.js';
import { PonyfillFormData } from '../src/FormData.js';
import { PonyfillReadableStream } from '../src/ReadableStream.js';
import { PonyfillURL } from '../src/URL.js';

describe('Node Fetch Ponyfill', () => {
runTestsForEachFetchImpl(() => {
Expand Down Expand Up @@ -171,5 +174,47 @@ describe('Node Fetch Ponyfill', () => {
const body = await response.json();
expect(body.swagger).toBe('2.0');
});
it('should handle object urls for PonyfillBlob', async () => {
const testJsonBlob = new PonyfillBlob([JSON.stringify({ test: 'test' })], {
type: 'application/json',
});
const objectUrl = PonyfillURL.createObjectURL(testJsonBlob);
const response = await fetchPonyfill(objectUrl);
expect(response.status).toBe(200);
expect([...response.headers]).toEqual([
['content-type', 'application/json'],
['content-length', '15'],
]);
const resJson = await response.json();
expect(resJson.test).toBe('test');
});
it('should handle object urls for global Blob', async () => {
const testJsonBlob = new globalThis.Blob([JSON.stringify({ test: 'test' })], {
type: 'application/json',
});
const objectUrl = URL.createObjectURL(testJsonBlob);
const response = await fetchPonyfill(objectUrl);
expect(response.status).toBe(200);
expect([...response.headers]).toEqual([
['content-type', 'application/json'],
['content-length', '15'],
]);
const resJson = await response.json();
expect(resJson.test).toBe('test');
});
it('should handle object urls for Node.js Blob', async () => {
const testJsonBlob = new NodeBlob([JSON.stringify({ test: 'test' })], {
type: 'application/json',
});
const objectUrl = NodeURL.createObjectURL(testJsonBlob);
const response = await fetchPonyfill(objectUrl);
expect(response.status).toBe(200);
expect([...response.headers]).toEqual([
['content-type', 'application/json'],
['content-length', '15'],
]);
const resJson = await response.json();
expect(resJson.test).toBe('test');
});
});
});

0 comments on commit fa097a4

Please sign in to comment.