Skip to content

Commit

Permalink
Merge pull request #1379 from tt-public/feat/request-formdata
Browse files Browse the repository at this point in the history
feat: Adds support for application/x-www-form-urlencoded in Request.formData
  • Loading branch information
capricorn86 committed Apr 4, 2024
2 parents 75041b2 + a642973 commit 5e160ed
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 41 deletions.
55 changes: 38 additions & 17 deletions packages/happy-dom/src/fetch/Request.ts
Expand Up @@ -304,29 +304,50 @@ export default class Request implements Request {
* @returns FormData.
*/
public async formData(): Promise<FormData> {
if (this.bodyUsed) {
throw new DOMException(
`Body has already been used for "${this.url}".`,
DOMExceptionNameEnum.invalidStateError
);
}
const contentType = this[PropertySymbol.contentType];
const asyncTaskManager = this.#asyncTaskManager;

(<boolean>this.bodyUsed) = true;
if (/multipart/i.test(contentType)) {
if (this.bodyUsed) {
throw new DOMException(
`Body has already been used for "${this.url}".`,
DOMExceptionNameEnum.invalidStateError
);
}

const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]());
let formData: FormData;
(<boolean>this.bodyUsed) = true;

try {
const type = this[PropertySymbol.contentType];
formData = (await MultipartFormDataParser.streamToFormData(this.body, type)).formData;
} catch (error) {
this.#asyncTaskManager.endTask(taskID);
throw error;
const taskID = asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]());
let formData: FormData;

try {
const result = await MultipartFormDataParser.streamToFormData(this.body, contentType);
formData = result.formData;
} catch (error) {
asyncTaskManager.endTask(taskID);
throw error;
}

asyncTaskManager.endTask(taskID);

return formData;
}

this.#asyncTaskManager.endTask(taskID);
if (contentType?.startsWith('application/x-www-form-urlencoded')) {
const parameters = new URLSearchParams(await this.text());
const formData = new FormData();

for (const [key, value] of parameters) {
formData.append(key, value);
}

return formData;
return formData;
}

throw new DOMException(
`Failed to construct FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`,
DOMExceptionNameEnum.invalidStateError
);
}

/**
Expand Down
57 changes: 36 additions & 21 deletions packages/happy-dom/src/fetch/Response.ts
Expand Up @@ -225,37 +225,52 @@ export default class Response implements Response {
*/
public async formData(): Promise<FormData> {
const contentType = this.headers.get('Content-Type');
const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager];

if (/multipart/i.test(contentType)) {
if (this.bodyUsed) {
throw new DOMException(
`Body has already been used for "${this.url}".`,
DOMExceptionNameEnum.invalidStateError
);
}

if (contentType?.startsWith('application/x-www-form-urlencoded')) {
const formData = new FormData();
const text = await this.text();
const parameters = new URLSearchParams(text);
(<boolean>this.bodyUsed) = true;

for (const [name, value] of parameters) {
formData.append(name, value);
const taskID = asyncTaskManager.startTask();
let formData: FormData;
let buffer: Buffer;

try {
const result = await MultipartFormDataParser.streamToFormData(this.body, contentType);
formData = result.formData;
buffer = result.buffer;
} catch (error) {
asyncTaskManager.endTask(taskID);
throw error;
}

this.#storeBodyInCache(buffer);
asyncTaskManager.endTask(taskID);

return formData;
}

const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask();
let formData: FormData;
let buffer: Buffer;

try {
const result = await MultipartFormDataParser.streamToFormData(this.body, contentType);
formData = result.formData;
buffer = result.buffer;
} catch (error) {
this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
throw error;
}
if (contentType?.startsWith('application/x-www-form-urlencoded')) {
const parameters = new URLSearchParams(await this.text());
const formData = new FormData();

this.#storeBodyInCache(buffer);
for (const [key, value] of parameters) {
formData.append(key, value);
}

this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID);
return formData;
}

return formData;
throw new DOMException(
`Failed to build FormData object: The "content-type" header is neither "application/x-www-form-urlencoded" nor "multipart/form-data".`,
DOMExceptionNameEnum.invalidStateError
);
}

/**
Expand Down
102 changes: 99 additions & 3 deletions packages/happy-dom/test/fetch/Request.test.ts
Expand Up @@ -15,6 +15,9 @@ import MultipartFormDataParser from '../../src/fetch/multipart/MultipartFormData
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest';
import { ReadableStream } from 'stream/web';
import * as PropertySymbol from '../../src/PropertySymbol.js';
import File from '../../src/file/File.js';
import Path from 'path';
import FS from 'fs';

const TEST_URL = 'https://example.com/';

Expand Down Expand Up @@ -666,13 +669,106 @@ describe('Request', () => {
});

describe('formData()', () => {
it('Returns FormData', async () => {
it('Returns FormData for FormData object (multipart)', async () => {
const formData = new FormData();
formData.append('some', 'test');
const request = new window.Request(TEST_URL, { method: 'POST', body: formData });
const requestFormData = await request.formData();
const formDataResponse = await request.formData();

expect(requestFormData).toEqual(formData);
expect(formDataResponse).toEqual(formData);
});

it('Returns FormData for URLSearchParams object (application/x-www-form-urlencoded)', async () => {
const urlSearchParams = new URLSearchParams();
urlSearchParams.append('some', 'test');
const request = new window.Request(TEST_URL, { method: 'POST', body: urlSearchParams });
const formDataResponse = await request.formData();

expect(formDataResponse instanceof FormData).toBe(true);
expect(formDataResponse.get('some')).toBe('test');
});

it('Returns FormData for "application/x-www-form-urlencoded" content.', async () => {
const urlSearchParams = new URLSearchParams();

urlSearchParams.set('key1', 'value1');
urlSearchParams.set('key2', 'value2');
urlSearchParams.set('key3', 'value3');

const request = new window.Request(TEST_URL, { method: 'POST', body: urlSearchParams });
const formDataResponse = await request.formData();
let size = 0;

for (const _entry of formDataResponse) {
size++;
}

expect(formDataResponse.get('key1')).toBe('value1');
expect(formDataResponse.get('key2')).toBe('value2');
expect(formDataResponse.get('key3')).toBe('value3');
expect(size).toBe(3);
});

it('Returns FormData for multipart text fields.', async () => {
const formData = new FormData();

vi.spyOn(Math, 'random').mockImplementation(() => 0.8);

formData.set('key1', 'value1');
formData.set('key2', 'value2');
formData.set('key3', 'value3');

const request = new window.Request(TEST_URL, { method: 'POST', body: formData });
const formDataResponse = await request.formData();
let size = 0;

for (const _entry of formDataResponse) {
size++;
}

expect(formDataResponse.get('key1')).toBe('value1');
expect(formDataResponse.get('key2')).toBe('value2');
expect(formDataResponse.get('key3')).toBe('value3');
expect(size).toBe(3);
});

it('Returns FormData for multipart files.', async () => {
const formData = new FormData();
const imageBuffer = await FS.promises.readFile(
Path.join(__dirname, 'data', 'test-image.jpg')
);

vi.spyOn(Math, 'random').mockImplementation(() => 0.8);

formData.set('key1', 'value1');
formData.set('file1', new File([imageBuffer], 'test-image-1.jpg', { type: 'image/jpeg' }));
formData.set('key2', 'value2');
formData.set('file2', new File([imageBuffer], 'test-image-2.jpg', { type: 'image/jpeg' }));

const request = new window.Request(TEST_URL, { method: 'POST', body: formData });
const formDataResponse = await request.formData();
let size = 0;

for (const _entry of formDataResponse) {
size++;
}

expect(formDataResponse.get('key1')).toBe('value1');
expect(formDataResponse.get('key2')).toBe('value2');
expect(size).toBe(4);

const file1 = <File>formDataResponse.get('file1');
const file2 = <File>formDataResponse.get('file2');

expect(file1.name).toBe('test-image-1.jpg');
expect(file1.type).toBe('image/jpeg');
expect(file1.size).toBe(imageBuffer.length);
expect(await file1.arrayBuffer()).toEqual(imageBuffer.buffer);

expect(file2.name).toBe('test-image-2.jpg');
expect(file2.type).toBe('image/jpeg');
expect(file2.size).toBe(imageBuffer.length);
expect(await file2.arrayBuffer()).toEqual(imageBuffer.buffer);
});

it('Supports window.happyDOM?.waitUntilComplete().', async () => {
Expand Down
19 changes: 19 additions & 0 deletions packages/happy-dom/test/fetch/Response.test.ts
Expand Up @@ -321,6 +321,25 @@ describe('Response', () => {
});

describe('formData()', () => {
it('Returns FormData for FormData object (multipart)', async () => {
const formData = new FormData();
formData.append('some', 'test');
const response = new window.Response(formData);
const formDataResponse = await response.formData();

expect(formDataResponse).toEqual(formData);
});

it('Returns FormData for URLSearchParams object (application/x-www-form-urlencoded)', async () => {
const urlSearchParams = new URLSearchParams();
urlSearchParams.append('some', 'test');
const response = new window.Response(urlSearchParams);
const formDataResponse = await response.formData();

expect(formDataResponse instanceof FormData).toBe(true);
expect(formDataResponse.get('some')).toBe('test');
});

it('Returns FormData for "application/x-www-form-urlencoded" content.', async () => {
const urlSearchParams = new URLSearchParams();

Expand Down

0 comments on commit 5e160ed

Please sign in to comment.