Skip to content

Commit db04ce0

Browse files
committed
Add basic support for parsing multipart/form-data fetch responses
1 parent 009d879 commit db04ce0

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
evaluation of the proxy autoconf javascript code fixed (regression)
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
@@ -505,6 +505,45 @@ public void fetchPostFormData() throws Exception {
505505
.contains("HtmlUnit"));
506506
}
507507

508+
/**
509+
* @throws Exception if the test fails
510+
*/
511+
@Test
512+
@Disabled
513+
@Alerts({"200", "OK", "true"})
514+
public void fetchMultipartFormData() throws Exception {
515+
final String html = DOCTYPE_HTML
516+
+ "<html>\n"
517+
+ " <body>\n"
518+
+ " <script>\n"
519+
+ LOG_TITLE_FUNCTION_NORMALIZE
520+
+ " fetch('" + URL_SECOND + "')"
521+
+ " .then(response => {\n"
522+
+ " log(response.status);\n"
523+
+ " log(response.statusText);\n"
524+
+ " log(response.ok);\n"
525+
+ " })\n"
526+
+ " .then(response => {\n"
527+
+ " return response.formData();\n"
528+
+ " })\n"
529+
+ " .then(formData => {\n"
530+
+ " log(formData.get('test0'));\n"
531+
+ " log(formData.get('test1'));\n"
532+
+ " })\n"
533+
+ " .catch(e => logEx(e));\n"
534+
+ " </script>\n"
535+
+ " </body>\n"
536+
+ "</html>";
537+
538+
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--";
539+
getMockWebConnection().setResponse(URL_SECOND, content, "multipart/form-data; boundary=0123456789");
540+
541+
final WebDriver driver = loadPage2(html);
542+
verifyTitle2(DEFAULT_WAIT_TIME, driver, getExpectedAlerts());
543+
544+
assertEquals(URL_SECOND, getMockWebConnection().getLastWebRequest().getUrl());
545+
}
546+
508547
/**
509548
* @throws Exception if the test fails
510549
*/

0 commit comments

Comments
 (0)