Skip to content

Commit 777105a

Browse files
das7padrbri
authored andcommitted
Add basic support for parsing multipart/form-data fetch responses
1 parent 802205a commit 777105a

File tree

5 files changed

+103
-1
lines changed

5 files changed

+103
-1
lines changed

src/changes/changes.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
<body>
1010
<release version="4.18.0" date="November xx, 2025" description="Chrome/Edge 141, Firefox 144, FirefoxESR 140, Bugfixes">
11+
<action type="add" dev="das7pad">
12+
Add basic support for parsing multipart/form-data fetch responses
13+
</action>
1114
<action type="fix" dev="rbri">
1215
XMLHttpRequest.getAllResponseHeaders() uses \r\n as delimiter in FF/FF_ESR also
1316
</action>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[fetch.umd.js]
2+
indent_size = 2

src/main/resources/org/htmlunit/javascript/polyfill/fetch/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Copyright (c) 2014-2016 GitHub, Inc.
2+
Copyright (c) 2025 Jakob Ackermann <[email protected]>
23

34
Permission is hereby granted, free of charge, to any person obtaining
45
a copy of this software and associated documentation files (the

src/main/resources/org/htmlunit/javascript/polyfill/fetch/fetch.umd.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,14 @@
322322

323323
if (support.formData) {
324324
this.formData = function() {
325-
return this.text().then(decode)
325+
var body = this;
326+
return this.text().then(function (text) {
327+
var contentType = body.headers.get('content-type') || '';
328+
if (contentType.indexOf('multipart/form-data') === 0) {
329+
return parseMultipart(contentType, text);
330+
}
331+
return decode(text)
332+
})
326333
};
327334
}
328335

@@ -419,6 +426,56 @@
419426
return form
420427
}
421428

429+
/**
430+
* @param header
431+
* @param parameter
432+
* @returns {string | undefined}
433+
*/
434+
function parseHeaderParameter(header, parameter) {
435+
var value
436+
header.split(';').forEach(function (param) {
437+
var keyVal = param.trim().split('=');
438+
if (keyVal.length > 1 && keyVal[0] === parameter) {
439+
value = keyVal[1];
440+
if (value.length > 1 && value[0] === '"' && value[value.length - 1] === '"') {
441+
value = value.slice(1, value.length - 1);
442+
}
443+
}
444+
})
445+
return value
446+
}
447+
448+
/**
449+
* @param {string} contentType
450+
* @param {string} text
451+
* @returns {FormData}
452+
*/
453+
function parseMultipart(contentType, text) {
454+
var boundary = parseHeaderParameter(contentType, "boundary");
455+
if (!boundary) {
456+
throw new Error('missing multipart/form-data boundary parameter')
457+
}
458+
var prefix = '--' + boundary + '\r\n'
459+
if (text.indexOf(prefix) !== 0) {
460+
throw new Error('multipart/form-data body must start with --boundary')
461+
}
462+
var suffix = '\r\n--' + boundary + '--'
463+
if (text.length < prefix.length + suffix.length || text.slice(text.length - suffix.length) !== suffix) {
464+
throw new Error('multipart/form-data body must end with --boundary--')
465+
}
466+
var formData = new FormData();
467+
text.slice(prefix.length, text.length - suffix.length).split('\r\n--' + boundary + '\r\n').forEach(function (part) {
468+
var headersEnd = part.indexOf('\r\n\r\n');
469+
if (headersEnd === -1) {
470+
throw new Error('multipart/form-data part is missing headers')
471+
}
472+
var headers = parseHeaders(part.slice(0, headersEnd));
473+
var name = parseHeaderParameter(headers.get('content-disposition'), 'name');
474+
formData.append(name, part.slice(headersEnd + 4))
475+
})
476+
return formData
477+
}
478+
422479
function parseHeaders(rawHeaders) {
423480
var headers = new Headers();
424481
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space

src/test/java/org/htmlunit/javascript/host/fetch/FetchTest.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,45 @@ public void fetchPostFormData() throws Exception {
537537
*/
538538
@Test
539539
@Alerts({"200", "OK", "true"})
540+
public void fetchMultipartFormData() throws Exception {
541+
final String html = DOCTYPE_HTML
542+
+ "<html>\n"
543+
+ " <body>\n"
544+
+ " <script>\n"
545+
+ LOG_TITLE_FUNCTION_NORMALIZE
546+
+ " fetch('" + URL_SECOND + "')"
547+
+ " .then(response => {\n"
548+
+ " log(response.status);\n"
549+
+ " log(response.statusText);\n"
550+
+ " log(response.ok);\n"
551+
+ " })\n"
552+
+ " .then(response => {\n"
553+
+ " return response.formData();\n"
554+
+ " })\n"
555+
+ " .then(formData => {\n"
556+
+ " log(formData.get('test0'));\n"
557+
+ " log(formData.get('test1'));\n"
558+
+ " })\n"
559+
+ " .catch(e => logEx(e));\n"
560+
+ " </script>\n"
561+
+ " </body>\n"
562+
+ "</html>";
563+
564+
final String content = "--0123456789\r\nContent-Disposition: form-data;name=test0\r\nContent-Type: text/plain\r\nHello1\nHello1\r\n--0123456789\r\nContent-Disposition: form-data;name=test1\r\nContent-Type: text/plain\r\nHello2\nHello2\r\n--0123456789--";
565+
getMockWebConnection().setResponse(URL_SECOND, content, "multipart/form-data; boundary=0123456789");
566+
567+
final WebDriver driver = loadPage2(html);
568+
verifyTitle2(DEFAULT_WAIT_TIME, driver, getExpectedAlerts());
569+
570+
assertEquals(URL_SECOND, getMockWebConnection().getLastWebRequest().getUrl());
571+
}
572+
573+
/**
574+
* @throws Exception if the test fails
575+
*/
576+
@Test
577+
@Disabled
578+
@Alerts({"200", "OK", "true"})
540579
public void fetchPostURLSearchParams() throws Exception {
541580
final String html = DOCTYPE_HTML
542581
+ "<html>\n"

0 commit comments

Comments
 (0)