Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

<body>
<release version="4.18.0" date="November xx, 2025" description="Chrome/Edge 141, Firefox 144, FirefoxESR 140, Bugfixes">
<action type="add" dev="das7pad">
Add basic support for parsing multipart/form-data fetch responses
</action>
<action type="fix" dev="rbri">
evaluation of the proxy autoconf javascript code fixed (regression)
</action>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[fetch.umd.js]
indent_size = 2
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Copyright (c) 2014-2016 GitHub, Inc.
Copyright (c) 2025 Jakob Ackermann <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,14 @@

if (support.formData) {
this.formData = function() {
return this.text().then(decode)
var body = this;
return this.text().then(function (text) {
var contentType = body.headers.get('content-type') || '';
if (contentType.indexOf('multipart/form-data') === 0) {
return parseMultipart(contentType, text);
}
return decode(text)
})
};
}

Expand Down Expand Up @@ -419,6 +426,56 @@
return form
}

/**
* @param header
* @param parameter
* @returns {string | undefined}
*/
function parseHeaderParameter(header, parameter) {
var value
header.split(';').forEach(function (param) {
var keyVal = param.trim().split('=');
if (keyVal.length > 1 && keyVal[0] === parameter) {
value = keyVal[1];
if (value.length > 1 && value[0] === '"' && value[value.length - 1] === '"') {
value = value.slice(1, value.length - 1);
}
}
})
return value
}

/**
* @param {string} contentType
* @param {string} text
* @returns {FormData}
*/
function parseMultipart(contentType, text) {
var boundary = parseHeaderParameter(contentType, "boundary");
if (!boundary) {
throw new Error('missing multipart/form-data boundary parameter')
}
var prefix = '--' + boundary + '\r\n'
if (text.indexOf(prefix) !== 0) {
throw new Error('multipart/form-data body must start with --boundary')
}
var suffix = '\r\n--' + boundary + '--'
if (text.length < prefix.length + suffix.length || text.slice(text.length - suffix.length) !== suffix) {
throw new Error('multipart/form-data body must end with --boundary--')
}
var formData = new FormData();
text.slice(prefix.length, text.length - suffix.length).split('\r\n--' + boundary + '\r\n').forEach(function (part) {
var headersEnd = part.indexOf('\r\n\r\n');
if (headersEnd === -1) {
throw new Error('multipart/form-data part is missing headers')
}
var headers = parseHeaders(part.slice(0, headersEnd));
var name = parseHeaderParameter(headers.get('content-disposition'), 'name');
formData.append(name, part.slice(headersEnd + 4))
})
return formData
}

function parseHeaders(rawHeaders) {
var headers = new Headers();
// Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space
Expand Down
39 changes: 39 additions & 0 deletions src/test/java/org/htmlunit/javascript/host/fetch/FetchTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,45 @@
.contains("HtmlUnit"));
}

/**
* @throws Exception if the test fails
*/
@Test
@Disabled
@Alerts({"200", "OK", "true"})
public void fetchMultipartFormData() throws Exception {
final String html = DOCTYPE_HTML
+ "<html>\n"
+ " <body>\n"
+ " <script>\n"
+ LOG_TITLE_FUNCTION_NORMALIZE
+ " fetch('" + URL_SECOND + "')"
+ " .then(response => {\n"
+ " log(response.status);\n"
+ " log(response.statusText);\n"
+ " log(response.ok);\n"
+ " })\n"
+ " .then(response => {\n"
+ " return response.formData();\n"
+ " })\n"
+ " .then(formData => {\n"
+ " log(formData.get('test0'));\n"
+ " log(formData.get('test1'));\n"
+ " })\n"
+ " .catch(e => logEx(e));\n"
+ " </script>\n"
+ " </body>\n"
+ "</html>";

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--";

Check failure on line 538 in src/test/java/org/htmlunit/javascript/host/fetch/FetchTest.java

View workflow job for this annotation

GitHub Actions / CheckStyle

[checkstyle] reported by reviewdog 🐶 Line is longer than 120 characters (found 262). Raw Output: /home/runner/work/htmlunit/htmlunit/src/test/java/org/htmlunit/javascript/host/fetch/FetchTest.java:538:0: error: Line is longer than 120 characters (found 262). (com.puppycrawl.tools.checkstyle.checks.sizes.LineLengthCheck)
getMockWebConnection().setResponse(URL_SECOND, content, "multipart/form-data; boundary=0123456789");

final WebDriver driver = loadPage2(html);
verifyTitle2(DEFAULT_WAIT_TIME, driver, getExpectedAlerts());

assertEquals(URL_SECOND, getMockWebConnection().getLastWebRequest().getUrl());
}

/**
* @throws Exception if the test fails
*/
Expand Down
Loading