GET /multipart
Returns a multipart/form-data response with RFC 7578 parser-adversity. Default declares one boundary in the Content-Type and uses a different one in the body. Use ?mode= to isolate other violations: missing terminating CRLF, ambiguous Content-Disposition `name=` parameters, or nested multipart bodies.
mode
boundary-mismatch (default; Content-Type declares one boundary but the body uses another; strict parsers find no opening delimiter and return zero parts, lenient parsers may scan and recover), trailing-crlf-confusion (closing boundary `--boundary-xyz--` has no trailing CRLF; strict parsers treat the stream as unterminated, lenient parsers accept), disposition-name-injection (Content-Disposition has two `name=` parameters; RFC 7578 does not define precedence and parsers disagree on which wins), nested-multipart (inner part uses Content-Type: multipart/mixed with its own boundary; most form-data parsers do not recurse into nested multipart and return the raw bytes or error).
control Compare against the well-formed counterpart: not.catastrophic.io/multipart side-by-side
build a request:
expect: A multipart/form-data response with one RFC 7578 violation. Strict parsers raise; lenient parsers recover differently — silent shape divergence is the typical outcome.
curl -si 'https://chaos.catastrophic.io/multipart?mode=boundary-mismatch'
import urllib.request, email
resp = urllib.request.urlopen("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch")
ct = resp.headers.get("Content-Type")
print("Content-Type:", ct)
print("X-Chaos-Multipart-Mode:", resp.headers.get("X-Chaos-Multipart-Mode"))
print("X-Chaos-Multipart-Note:", resp.headers.get("X-Chaos-Multipart-Note"))
raw = resp.read()
# Reconstruct as a MIME message so email.parser can split parts.
msg = email.message_from_bytes(b"Content-Type: " + ct.encode() + b"\r\n\r\n" + raw)
parts = msg.get_payload() if msg.is_multipart() else None
print("parts parsed:", len(parts) if parts else "n/a")
// Node 18+: Response from undici can parse multipart via .formData()
const res = await fetch("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch");
console.log("Content-Type:", res.headers.get("content-type"));
console.log("X-Chaos-Multipart-Mode:", res.headers.get("x-chaos-multipart-mode"));
console.log("X-Chaos-Multipart-Note:", res.headers.get("x-chaos-multipart-note"));
try {
const fd = await res.formData();
for (const [k, v] of fd) console.log(` part: ${k} =`, typeof v === "string" ? v : ``);
} catch (e) {
console.log("formData parse failed:", e.message);
console.log("raw body:", await res.text());
}
package main
import (
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"strings"
)
func main() {
resp, _ := http.Get("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch")
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
fmt.Println("Content-Type:", ct)
fmt.Println("X-Chaos-Multipart-Mode:", resp.Header.Get("X-Chaos-Multipart-Mode"))
_, params, _ := mime.ParseMediaType(ct)
mr := multipart.NewReader(resp.Body, params["boundary"])
for {
p, err := mr.NextPart()
if err == io.EOF { break }
if err != nil { fmt.Println("part error:", err); break }
body, _ := io.ReadAll(p)
fmt.Printf(" part: name=%q len=%d\n", p.FormName(), len(body))
_ = strings.TrimSpace
}
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
// Multipart parsing on the response side typically needs a separate crate
// (e.g. multer); this example just dumps the raw body.
fn main() -> Result<(), Box> {
let resp = reqwest::blocking::get("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch")?;
println!("Content-Type: {:?}", resp.headers().get("content-type"));
println!("X-Chaos-Multipart-Mode: {:?}", resp.headers().get("x-chaos-multipart-mode"));
println!("--- raw body ---\n{}", resp.text()?);
Ok(())
}
import java.net.URI;
import java.net.http.*;
public class MultipartChaos {
public static void main(String[] args) throws Exception {
var client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch")).build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Content-Type: " +
resp.headers().firstValue("Content-Type").orElse(""));
System.out.println("X-Chaos-Multipart-Mode: " +
resp.headers().firstValue("X-Chaos-Multipart-Mode").orElse(""));
System.out.println("Body: " + resp.body());
}
}
using var client = new HttpClient();
var resp = await client.GetAsync("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch");
Console.WriteLine($"Content-Type: {resp.Content.Headers.ContentType}");
Console.WriteLine($"X-Chaos-Multipart-Mode: " +
$"{resp.Headers.GetValues("X-Chaos-Multipart-Mode").FirstOrDefault()}");
var body = await resp.Content.ReadAsStringAsync();
Console.WriteLine($"Body: {body}");
require "net/http"
uri = URI("https://chaos.catastrophic.io/multipart?mode=boundary-mismatch")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
res = http.get(uri.request_uri)
puts "Content-Type: #{res['Content-Type']}"
puts "X-Chaos-Multipart-Mode: #{res['X-Chaos-Multipart-Mode']}"
puts "X-Chaos-Multipart-Note: #{res['X-Chaos-Multipart-Note']}"
puts res.body
end
$r = Invoke-WebRequest -Uri 'https://chaos.catastrophic.io/multipart?mode=boundary-mismatch'
"Content-Type: $($r.Headers['Content-Type'])"
"X-Chaos-Multipart-Mode: $($r.Headers['X-Chaos-Multipart-Mode'])"
"X-Chaos-Multipart-Note: $($r.Headers['X-Chaos-Multipart-Note'])"
$r.Content
headers
body