Skip to content

Commit baaeeb9

Browse files
authored
fix: [#1741] Adds missing end boundary to FormData requests (#1742)
1 parent d66de2d commit baaeeb9

File tree

6 files changed

+56
-58
lines changed

6 files changed

+56
-58
lines changed

packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts

+3
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export default class MultipartFormDataParser {
120120
}
121121
}
122122

123+
// add end boundary
124+
chunks.push(Buffer.from(`--${boundary}--\r\n`));
125+
123126
const buffer = Buffer.concat(chunks);
124127

125128
return {

packages/happy-dom/src/fetch/multipart/MultipartReader.ts

+30-35
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js';
21
import File from '../../file/File.js';
32
import FormData from '../../form-data/FormData.js';
43
import BrowserWindow from '../../window/BrowserWindow.js';
54

6-
enum MultiparParserStateEnum {
5+
enum MultipartParserStateEnum {
76
boundary = 0,
87
headerStart = 2,
98
header = 3,
@@ -25,7 +24,7 @@ export default class MultipartReader {
2524
private formData: FormData;
2625
private boundary: Uint8Array;
2726
private boundaryIndex = 0;
28-
private state = MultiparParserStateEnum.boundary;
27+
private state = MultipartParserStateEnum.boundary;
2928
private data: {
3029
contentDisposition: { [key: string]: string } | null;
3130
value: number[];
@@ -37,7 +36,6 @@ export default class MultipartReader {
3736
contentType: null,
3837
header: ''
3938
};
40-
private window: BrowserWindow;
4139

4240
/**
4341
* Constructor.
@@ -48,7 +46,6 @@ export default class MultipartReader {
4846
*/
4947
constructor(window: BrowserWindow, boundary: string) {
5048
const boundaryHeader = `--${boundary}`;
51-
this.window = window;
5249
this.boundary = new Uint8Array(boundaryHeader.length);
5350
this.formData = new window.FormData();
5451

@@ -71,64 +68,66 @@ export default class MultipartReader {
7168
nextChar = data[i + 1];
7269

7370
switch (this.state) {
74-
case MultiparParserStateEnum.boundary:
71+
case MultipartParserStateEnum.boundary:
7572
if (char === this.boundary[this.boundaryIndex]) {
7673
this.boundaryIndex++;
7774
} else {
7875
this.boundaryIndex = 0;
7976
}
8077

8178
if (this.boundaryIndex === this.boundary.length) {
82-
this.state = MultiparParserStateEnum.headerStart;
79+
this.state = MultipartParserStateEnum.headerStart;
8380
this.boundaryIndex = 0;
8481
}
8582

8683
break;
8784

88-
case MultiparParserStateEnum.headerStart:
85+
case MultipartParserStateEnum.headerStart:
8986
if (nextChar !== CHARACTER_CODE.cr && nextChar !== CHARACTER_CODE.lf) {
9087
this.data.header = '';
9188
this.state =
9289
data[i - 2] === CHARACTER_CODE.lf
93-
? MultiparParserStateEnum.data
94-
: MultiparParserStateEnum.header;
90+
? MultipartParserStateEnum.data
91+
: MultipartParserStateEnum.header;
9592
}
9693

9794
break;
9895

99-
case MultiparParserStateEnum.header:
96+
case MultipartParserStateEnum.header:
10097
if (char === CHARACTER_CODE.cr) {
10198
if (this.data.header) {
10299
const headerParts = this.data.header.split(':');
103-
const headerName = headerParts[0].toLowerCase();
104-
const headerValue = headerParts[1].trim();
105-
106-
switch (headerName) {
107-
case 'content-disposition':
108-
this.data.contentDisposition = this.getContentDisposition(headerValue);
109-
break;
110-
case 'content-type':
111-
this.data.contentType = headerValue;
112-
break;
100+
if (headerParts.length > 1) {
101+
const headerName = headerParts[0].toLowerCase();
102+
const headerValue = headerParts[1].trim();
103+
104+
switch (headerName) {
105+
case 'content-disposition':
106+
this.data.contentDisposition = this.getContentDisposition(headerValue);
107+
break;
108+
case 'content-type':
109+
this.data.contentType = headerValue;
110+
break;
111+
}
113112
}
114113
}
115114

116-
this.state = MultiparParserStateEnum.headerStart;
115+
this.state = MultipartParserStateEnum.headerStart;
117116
} else {
118117
this.data.header += String.fromCharCode(char);
119118
}
120119

121120
break;
122121

123-
case MultiparParserStateEnum.data:
122+
case MultipartParserStateEnum.data:
124123
if (char === this.boundary[this.boundaryIndex]) {
125124
this.boundaryIndex++;
126125
} else {
127126
this.boundaryIndex = 0;
128127
}
129128

130129
if (this.boundaryIndex === this.boundary.length) {
131-
this.state = MultiparParserStateEnum.headerStart;
130+
this.state = MultipartParserStateEnum.headerStart;
132131

133132
if (this.data.value.length) {
134133
this.appendFormData(
@@ -159,20 +158,16 @@ export default class MultipartReader {
159158
* @returns Form data.
160159
*/
161160
public end(): FormData {
162-
if (this.state !== MultiparParserStateEnum.data) {
163-
throw new this.window.DOMException(
164-
`Unexpected end of multipart stream. Expected state to be "${MultiparParserStateEnum.data}" but got "${this.state}".`,
165-
DOMExceptionNameEnum.invalidStateError
161+
// If we are missing an end boundary, but we have data, we should append it.
162+
if (this.data.contentDisposition && this.data.value.length) {
163+
this.appendFormData(
164+
this.data.contentDisposition.name,
165+
Buffer.from(this.data.value.slice(0, -2)),
166+
this.data.contentDisposition.filename,
167+
this.data.contentType
166168
);
167169
}
168170

169-
this.appendFormData(
170-
this.data.contentDisposition.name,
171-
Buffer.from(this.data.value.slice(0, -2)),
172-
this.data.contentDisposition.filename,
173-
this.data.contentType
174-
);
175-
176171
return this.formData;
177172
}
178173

packages/happy-dom/test/fetch/Fetch.test.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -3339,7 +3339,7 @@ describe('Fetch', () => {
33393339
'Accept-Encoding': 'gzip, deflate, br',
33403340
'Content-Type':
33413341
'multipart/form-data; boundary=----HappyDOMFormDataBoundary0.ssssssssst',
3342-
'Content-Length': '198'
3342+
'Content-Length': '244'
33433343
},
33443344
agent: false,
33453345
rejectUnauthorized: true,
@@ -3349,7 +3349,7 @@ describe('Fetch', () => {
33493349
});
33503350
expect(destroyCount).toBe(1);
33513351
expect(writtenBodyData).toBe(
3352-
'------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n'
3352+
'------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n------HappyDOMFormDataBoundary0.ssssssssst--\r\n'
33533353
);
33543354
expect(response.status).toBe(200);
33553355
});
@@ -3627,7 +3627,7 @@ describe('Fetch', () => {
36273627
'content-length',
36283628
String(responseText.length),
36293629
'cache-control',
3630-
'max-age=0.001',
3630+
'max-age=0.0001',
36313631
'last-modified',
36323632
'Mon, 11 Dec 2023 01:00:00 GMT'
36333633
];
@@ -3672,7 +3672,7 @@ describe('Fetch', () => {
36723672
expect(headers1).toEqual({
36733673
'content-type': 'text/html',
36743674
'content-length': String(responseText.length),
3675-
'cache-control': `max-age=0.001`,
3675+
'cache-control': `max-age=0.0001`,
36763676
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT'
36773677
});
36783678

@@ -3786,7 +3786,7 @@ describe('Fetch', () => {
37863786
'content-length',
37873787
String(responseText1.length),
37883788
'cache-control',
3789-
'max-age=0.001',
3789+
'max-age=0.0001',
37903790
'last-modified',
37913791
'Mon, 11 Dec 2023 01:00:00 GMT'
37923792
];
@@ -3839,7 +3839,7 @@ describe('Fetch', () => {
38393839
expect(headers1).toEqual({
38403840
'content-type': 'text/html',
38413841
'content-length': String(responseText1.length),
3842-
'cache-control': `max-age=0.001`,
3842+
'cache-control': `max-age=0.0001`,
38433843
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT'
38443844
});
38453845

@@ -3954,7 +3954,7 @@ describe('Fetch', () => {
39543954
'content-length',
39553955
String(responseText.length),
39563956
'cache-control',
3957-
'max-age=0.001',
3957+
'max-age=0.0001',
39583958
'last-modified',
39593959
'Mon, 11 Dec 2023 01:00:00 GMT',
39603960
'etag',
@@ -4004,7 +4004,7 @@ describe('Fetch', () => {
40044004
expect(headers1).toEqual({
40054005
'content-type': 'text/html',
40064006
'content-length': String(responseText.length),
4007-
'cache-control': `max-age=0.001`,
4007+
'cache-control': `max-age=0.0001`,
40084008
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT',
40094009
etag: etag1
40104010
});
@@ -4018,7 +4018,7 @@ describe('Fetch', () => {
40184018
expect(headers2).toEqual({
40194019
'content-type': 'text/html',
40204020
'content-length': String(responseText.length),
4021-
'cache-control': `max-age=0.001`,
4021+
'cache-control': `max-age=0.0001`,
40224022
'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT',
40234023
ETag: etag2
40244024
});
@@ -4124,7 +4124,7 @@ describe('Fetch', () => {
41244124
'content-length',
41254125
String(responseText1.length),
41264126
'cache-control',
4127-
'max-age=0.001',
4127+
'max-age=0.0001',
41284128
'last-modified',
41294129
'Mon, 11 Dec 2023 01:00:00 GMT',
41304130
'etag',
@@ -4171,7 +4171,7 @@ describe('Fetch', () => {
41714171
expect(headers1).toEqual({
41724172
'content-type': 'text/html',
41734173
'content-length': String(responseText1.length),
4174-
'cache-control': `max-age=0.001`,
4174+
'cache-control': `max-age=0.0001`,
41754175
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT',
41764176
etag: etag1
41774177
});

packages/happy-dom/test/fetch/SyncFetch.test.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -2590,7 +2590,7 @@ describe('SyncFetch', () => {
25902590
browserFrame.url = 'https://localhost:8080/';
25912591

25922592
const body =
2593-
'------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n';
2593+
'------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.ssssssssst\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n------HappyDOMFormDataBoundary0.ssssssssst--\r\n';
25942594
const formData = new window.FormData();
25952595
let requestArgs: string | null = null;
25962596

@@ -2803,7 +2803,7 @@ describe('SyncFetch', () => {
28032803
'content-length',
28042804
String(responseText.length),
28052805
'cache-control',
2806-
'max-age=0.001',
2806+
'max-age=0.0001',
28072807
'last-modified',
28082808
'Mon, 11 Dec 2023 01:00:00 GMT'
28092809
],
@@ -2849,7 +2849,7 @@ describe('SyncFetch', () => {
28492849
expect(headers1).toEqual({
28502850
'content-type': 'text/html',
28512851
'content-length': String(responseText.length),
2852-
'cache-control': `max-age=0.001`,
2852+
'cache-control': `max-age=0.0001`,
28532853
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT'
28542854
});
28552855

@@ -2940,7 +2940,7 @@ describe('SyncFetch', () => {
29402940
'content-length',
29412941
String(responseText1.length),
29422942
'cache-control',
2943-
'max-age=0.001',
2943+
'max-age=0.0001',
29442944
'last-modified',
29452945
'Mon, 11 Dec 2023 01:00:00 GMT'
29462946
],
@@ -2994,7 +2994,7 @@ describe('SyncFetch', () => {
29942994
expect(headers1).toEqual({
29952995
'content-type': 'text/html',
29962996
'content-length': String(responseText1.length),
2997-
'cache-control': `max-age=0.001`,
2997+
'cache-control': `max-age=0.0001`,
29982998
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT'
29992999
});
30003000

@@ -3085,7 +3085,7 @@ describe('SyncFetch', () => {
30853085
'content-length',
30863086
String(responseText.length),
30873087
'cache-control',
3088-
'max-age=0.001',
3088+
'max-age=0.0001',
30893089
'last-modified',
30903090
'Mon, 11 Dec 2023 01:00:00 GMT',
30913091
'etag',
@@ -3141,7 +3141,7 @@ describe('SyncFetch', () => {
31413141
expect(headers1).toEqual({
31423142
'content-type': 'text/html',
31433143
'content-length': String(responseText.length),
3144-
'cache-control': `max-age=0.001`,
3144+
'cache-control': `max-age=0.0001`,
31453145
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT',
31463146
etag: etag1
31473147
});
@@ -3155,7 +3155,7 @@ describe('SyncFetch', () => {
31553155
expect(headers2).toEqual({
31563156
'content-type': 'text/html',
31573157
'content-length': String(responseText.length),
3158-
'cache-control': `max-age=0.001`,
3158+
'cache-control': `max-age=0.0001`,
31593159
'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT',
31603160
ETag: etag2
31613161
});
@@ -3238,7 +3238,7 @@ describe('SyncFetch', () => {
32383238
'content-length',
32393239
String(responseText1.length),
32403240
'cache-control',
3241-
'max-age=0.001',
3241+
'max-age=0.0001',
32423242
'last-modified',
32433243
'Mon, 11 Dec 2023 01:00:00 GMT',
32443244
'etag',
@@ -3286,7 +3286,7 @@ describe('SyncFetch', () => {
32863286
expect(headers1).toEqual({
32873287
'content-type': 'text/html',
32883288
'content-length': String(responseText1.length),
3289-
'cache-control': `max-age=0.001`,
3289+
'cache-control': `max-age=0.0001`,
32903290
'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT',
32913291
etag: etag1
32923292
});

packages/integration-test/test/tests/Fetch.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('Fetch', () => {
7878
'----HappyDOMFormDataBoundary0.noRandom'
7979
)
8080
).toBe(
81-
'header:\nmultipart/form-data; boundary=----HappyDOMFormDataBoundary0.noRandom\n\nbody:\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n'
81+
'header:\nmultipart/form-data; boundary=----HappyDOMFormDataBoundary0.noRandom\n\nbody:\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n------HappyDOMFormDataBoundary0.noRandom--\r\n'
8282
);
8383
});
8484
});

packages/jest-environment/test/javascript/JavaScript.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('JavaScript', () => {
8484
'----HappyDOMFormDataBoundary0.noRandom'
8585
)
8686
).toBe(
87-
'header:\nmultipart/form-data; boundary=----HappyDOMFormDataBoundary0.noRandom\n\nbody:\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n'
87+
'header:\nmultipart/form-data; boundary=----HappyDOMFormDataBoundary0.noRandom\n\nbody:\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key1"\r\n\r\nvalue1\r\n------HappyDOMFormDataBoundary0.noRandom\r\nContent-Disposition: form-data; name="key2"\r\n\r\nvalue2\r\n------HappyDOMFormDataBoundary0.noRandom--\r\n'
8888
);
8989
});
9090

0 commit comments

Comments
 (0)