fix(http): join multipart request bodies with CRLF (#1802)

* fix(http): join multipart request bodies with CRLF

Multipart parts are delimited by CRLF per RFC 2046; prepare() joined
body lines with LF, so strict servers (Tomcat) never found the boundary
and rejected the request as 413. Join with CRLF when the request's
Content-Type is multipart/*; other bodies keep LF.

Fixes #1700

* ci: retrigger after flaky mybatis test on windows

* ci: retrigger after flaky java debug smoke
This commit is contained in:
arookieofc
2026-11-06 20:55:17 +08:00
committed by GitHub
parent 796f3f1512
commit e2d82e2ec8
+24 -1
View File
@@ -87,12 +87,19 @@ pub fn prepare(
} else {
with_scheme(&url)
};
// Multipart bodies delimit parts with CRLF per RFC 2046 §5.1; joining
// with bare LF leaves servers (e.g. Tomcat) unable to find the boundary.
let is_multipart = headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
.is_some_and(|(_, v)| v.to_ascii_lowercase().starts_with("multipart/"));
let eol = if is_multipart { "\r\n" } else { "\n" };
Ok(PreparedRequest {
name: raw.block.name.clone(),
method: raw.block.method.clone(),
url,
headers,
body: (!body_lines.is_empty()).then(|| body_lines.join("\n")),
body: (!body_lines.is_empty()).then(|| body_lines.join(eol)),
})
}
@@ -205,6 +212,22 @@ mod tests {
assert!(err.message.contains("host"), "{}", err.message);
}
#[test]
fn multipart_bodies_join_lines_with_crlf() {
let f = parse(
"POST https://api.test/upload\nContent-Type: multipart/form-data; boundary=xyz\n\n--xyz\nfield\n--xyz--\n",
);
let p = prepare(&f.requests[0], &f.variables, &Environments::default(), None, None).unwrap();
assert_eq!(p.body.as_deref(), Some("--xyz\r\nfield\r\n--xyz--"));
}
#[test]
fn non_multipart_bodies_join_lines_with_lf() {
let f = parse("POST https://api.test/x\nContent-Type: application/json\n\n{\n\"a\": 1\n}\n");
let p = prepare(&f.requests[0], &f.variables, &Environments::default(), None, None).unwrap();
assert_eq!(p.body.as_deref(), Some("{\n\"a\": 1\n}"));
}
#[test]
fn relative_urls_use_the_host_header() {
let f = parse("GET /status\nHost: localhost:9000\n");