Fix multipart/form-data 400 errors: boundary, CRLF & \r\r\n
RFC 2046 multipart parsers are strict about boundary delimiters and CRLF line endings. Here's how to spot boundary-prefix, LF, and \r\r\n bugs in Yellorn.
What you're seeing
Your webhook captured a request body, and the Yellorn inspector flagged its line-ending style as mixed or CR only. Switching the Req Body tab to Whitespace mode shows glyphs like ␍ (carriage return), ␊ (line feed), or the bug-pattern combination ␍␍␊ at the end of one or more lines.
If the receiver of that body is a strict multipart parser — most server frameworks, every CDN's edge multipart handler, and every spec-compliant SDK — the body is being rejected with a 400 Bad Request, a 415 Unsupported Media Type, or an even less helpful generic "could not parse form" error. The confusing part: the raw text looks fine. Line endings are invisible in a plain <pre> block, so the smoking gun only shows up once you reveal the whitespace.

What multipart/form-data requires
RFC 2046 §5.1 (the multipart media-type standard, referenced by RFC 7578 for multipart/form-data specifically) is explicit: every boundary, header line, and the header-to-body separator inside a multipart body must end with CRLF — the two-byte sequence 0x0D 0x0A (\r\n).
A correctly formed part looks like this when invisible characters are revealed:
--BOUNDARY␍␊ Content-Disposition: form-data; name="file"; filename="x.pdf"␍␊ Content-Type: application/pdf␍␊ ␍␊ <binary bytes here>␍␊ --BOUNDARY--␍␊
Note the empty ␍␊ line between the headers and the body — that's the spec-mandated separator, consisting of exactly one CRLF on its own line. Two CRLFs back-to-back. Not \r\n\n, not \n\n, not \r\r\n.
Boundary delimiter prefix rule
RFC 2046 also has a second rule that often hides behind vague parser errors: the chosen boundary delimiter must not appear as the prefix of any line inside a part. If your boundary is BOUNDARY, then a file or text field containing a line that starts with --BOUNDARY can be mistaken for the next part delimiter.
This is why parser traces sometimes mention low-level scanner helpers such as ap_lua_binstrstr or quote the RFC text "boundary delimiter must not appear as the prefix of any line". The fix is the same one robust clients already use: let FormData, requests, curl -F, or mime/multipart generate a long random boundary instead of hand-picking a short human-readable string.
Why mixed line endings break parsers
Multipart parsers do not guess. They scan the byte stream for the literal boundary sequence \r\n--BOUNDARY\r\n and reject anything that doesn't match the expected envelope. Three failure modes are common:
- Lone
\n(Unix line ending) — a permissive parser may accept the part, but strict parsers (Go'smime/multipart, Rust'smulter, several Cloudflare Workers helpers) return "unexpected EOF" because they never find the CRLF-prefixed boundary. - Lone
\rfollowed by\r\n(the\r\r\nbug) — almost always caused by an HTTP client that double-encoded the line separator. Parsers see the first\ras the start of a CRLF and never find the matching\n; the body looks corrupt from byte 0. - Mixed CRLF and LF in the same body — boundary lines use CRLF, header lines use LF (or vice versa). The boundary scanner finds the first boundary correctly, then misses the second because its byte prefix doesn't match.
None of these failures surface a useful diagnostic. That's why the Yellorn inspector calls them out explicitly — the bug is invisible until you look for it.
Common causes
Manually-constructed multipart bodies
Anywhere you wrote `--BOUNDARY\nContent-Disposition…\n` by hand, you almost certainly produced an LF-only body. Most languages' string literals don't include CRLF unless you explicitly write \r\n.
Double-encoding line separators
Producing a body in code that already uses CRLF, then running it through a library that prepends another \r at every line boundary, yields \r\r\n across the entire body. Common offenders: Windows file readers run in binary mode and then re-written through a text-mode helper, manually re-joined headers with "\r" + "\r\n", badly-configured proxy intermediaries.
Wrong cURL flag
curl --data (or -d) strips newlines silently. curl --data-binary preserves the body exactly as it appears on disk. If you're piping a captured request through cURL, always use --data-binary.
Boundary mismatch
Less common but related: the boundary= parameter in the Content-Type header doesn't match the literal boundary used in the body, or the body's boundary is missing the leading -- prefix that RFC 2046 mandates for the in-body marker. Yellorn's stats line surfaces the parsed boundary so you can spot this at a glance.
How to fix it (by language)
JavaScript / TypeScript (fetch)
Always use FormData — never serialize a multipart body by hand. The browser's implementation gets line endings, encoding, and boundary generation right every time.
const fd = new FormData();
fd.append("file", fileBlob, "report.pdf");
fd.append("title", "Q3 numbers");
await fetch(url, {
method: "POST",
body: fd,
// Do NOT set Content-Type manually — fetch derives it
// (including the boundary) from the FormData instance.
});Python (requests)
Use the files= parameter, not data=. requests handles CRLF and boundary generation internally.
import requests
with open("report.pdf", "rb") as f:
response = requests.post(
url,
files={"file": ("report.pdf", f, "application/pdf")},
data={"title": "Q3 numbers"},
)Python (urllib / hand-rolled)
If you must construct the body yourself (testing, weird edge cases), use b"\r\n" — bytes, not str — for every separator:
CRLF = b"\r\n"
boundary = b"----yellorn-example"
body = (
b"--" + boundary + CRLF
+ b'Content-Disposition: form-data; name="file"; filename="x.pdf"' + CRLF
+ b"Content-Type: application/pdf" + CRLF
+ CRLF # blank line separating headers from body
+ payload_bytes + CRLF
+ b"--" + boundary + b"--" + CRLF
)cURL
Prefer -F (form upload) — it builds a spec-compliant body for you:
curl -X POST "$URL" \ -F "file=@report.pdf;type=application/pdf" \ -F "title=Q3 numbers"
When you have a captured raw body to replay, use --data-binary @file (not --data):
curl -X POST "$URL" \ -H "Content-Type: multipart/form-data; boundary=$BOUNDARY" \ --data-binary @captured-body.bin
Go
Use mime/multipart; the writer takes care of CRLF + boundary placement.
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
part, _ := w.CreateFormFile("file", "report.pdf")
io.Copy(part, fileReader)
_ = w.WriteField("title", "Q3 numbers")
_ = w.Close()
req, _ := http.NewRequest("POST", url, &buf)
req.Header.Set("Content-Type", w.FormDataContentType())Jinja templates (and similar templating engines)
Jinja and similar engines (used by API request builders, integration platforms, no-code workflows) render newlines as LF by default — so a body that looks right in the editor is wrong on the wire. Try POSIX style first; if the receiver rejects it, switch to the Windows-style version below. The Windows style always works.
POSIX style — try this first. Write the body naturally; newlines render as LF.
--MyBoundary
Content-Disposition: form-data; name="example_key_1"
example_value_1
--MyBoundary
Content-Disposition: form-data; name="attachment"; filename="{{ resume_filename }}"
Content-Type: application/pdf
{{ binary_content }}
--MyBoundary--Windows style — use if POSIX is rejected. Declare a CRLF variable once, end every line with {{-br-}}:
{%- set br = "\r\n" | safe -%}
--MyBoundary{{-br-}}
Content-Disposition: form-data; name="example_key_1"{{-br-}}
{{-br-}}
example_value_1{{-br-}}
--MyBoundary{{-br-}}
Content-Disposition: form-data; name="attachment"; filename="{{ resume_filename }}"{{-br-}}
Content-Type: application/pdf{{-br-}}
{{-br-}}
{{ binary_content }}{{-br-}}
--MyBoundary--Why this works:
"\r\n" | safe— literal CRLF;|safestops Jinja from escaping\rto .{%- ... -%}/{{- ... -}}— the-modifiers strip the source whitespace (including the LF) around the tag, so only the injected CRLF survives.- One
{{-br-}}at every line end, including blank lines. The empty line between headers and body (RFC 2046 §5.1) is the most common slip — miss it and the Windows version still fails.
Same trap, different syntax in other engines: Liquid → {%- assign br = "\r\n" -%} with {{ br }}; Nunjucks → reuses Jinja's {%- -%} markers; Handlebars / Mustache → no trim modifiers, so do a server-side \n → \r\n pass after rendering. Pattern: define CRLF once, inject at every line, trim the source newlines.
Verify the fix with Yellorn
After patching the sender, replay the request against the same Yellorn webhook URL and open the request log entry. In the Req Body tab:
- The stats line should now read CRLF with no warning badge. A pure CRLF body is the canonical form RFC 2046 mandates.
- Click Whitespace. Every line should end with
␍␊(CRLF) — no bare␊(LF) and no lone␍(CR) anywhere in the body. - Click Hex. Spot-check a few line boundaries: the byte pair
0d 0ashould appear at the end of every header and after each boundary marker.
If any of these checks fails, the bug is still in the sender — not in the receiver. Yellorn shows you exactly what the receiver saw byte-for-byte, which is the ground-truth baseline for diagnosis.

Frequently asked questions
Why is my multipart/form-data request rejected with a 400 error?
The multipart standard (RFC 2046, used by RFC 7578) requires each delimiter line to start with CRLF followed by --boundary, and each part header must also end with CRLF. If your client emits lone LF, doubled \r\r\n, or a boundary-like prefix inside a part, strict parsers reject the body with an unhelpful 400 or 'could not parse form'.
What does the \r\r\n bug mean?
It's a doubled carriage return, almost always caused by code that double-encodes the line separator. The parser reads the first \r as the start of a CRLF, never finds the matching \n, and treats the body as corrupt from the first byte — even though the raw text looks fine because line endings are invisible.
What does 'boundary delimiter must not appear as the prefix of any line' mean?
RFC 2046 says the boundary delimiter chosen for a multipart body must not appear at the beginning of any line inside an encapsulated part. If a file or text field contains a line that starts with --your-boundary, the parser may treat user data as the next part delimiter. Generate a long random boundary and let FormData, requests, curl -F, or mime/multipart choose it for you.
How do I fix multipart line endings in my code?
Don't hand-build the body. Use the platform's multipart helper: FormData with fetch (and don't set Content-Type manually), files= with Python requests, -F with curl, or mime/multipart in Go. If you must template the body, inject a literal "\r\n" at every line end, including the blank line between headers and body.
How can I tell whether my body has the right line endings?
In Yellorn's webhook request log, open the Req Body tab and switch to Whitespace mode — every line should end with ␍␊ (CRLF), with no bare ␊ or lone ␍. Or switch to Hex and confirm the byte pair 0d 0a appears at the end of every header and boundary line.
Related
Where to go next
Try a fix in the editor or browse more articles.