What does a multipart/form-data body look like on the wire, and what are the boundary, CRLF and Content-Type rules you must get right?
Predict first
What bytes does the builder produce (⏎ marks CRLF)?
import Foundation
func multipart(boundary: String, fields: [String: String],
file: (name: String, filename: String, mime: String, data: Data)) -> Data {
var body = Data()
for (k, v) in fields.sorted(by: { $0.key < $1.key }) {
body.append(Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(k)\"\r\n\r\n\(v)\r\n".utf8))
}
body.append(Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.filename)\"\r\nContent-Type: \(file.mime)\r\n\r\n".utf8))
body.append(file.data)
body.append(Data("\r\n--\(boundary)--\r\n".utf8))
return body
}
let body = multipart(boundary: "XBOUNDARY", fields: ["title": "Hi"],
file: ("photo", "a.txt", "text/plain", Data("DATA".utf8)))
print(String(decoding: body, as: UTF8.self).replacingOccurrences(of: "\r\n", with: "⏎\n"))
print("bytes:", body.count)
- Two parts delimited by --XBOUNDARY, a blank line before each content, closing --XBOUNDARY--, 191 bytes
- Parts separated by blank lines only, 100 bytes
- JSON object with a file field
Mental model. A multipart body is a parcel with labelled compartments, each separated by a divider card that carries a unique code so it cannot be mistaken for a compartment’s contents.
A multipart body is a sequence of parts separated by a boundary string that must not appear in the data. Each part is --boundary, then headers (Content-Disposition: form-data; name="title", and for a file also filename="a.txt" and Content-Type: text/plain), then a blank line, then the content, then a line break. The body ends with --boundary--. All line breaks are CRLF (\r\n), not \n: servers are often strict. The request header must repeat the boundary: Content-Type: multipart/form-data; boundary=XBOUNDARY. The check prints the exact body (CRLF shown as ⏎): a text field, a file part with its bytes, the closing delimiter, 191 bytes in all.
Practical rules. Generate a random boundary per request (a UUID) so it cannot collide with content. Do not assemble large files into memory: write the body to a temporary file and use uploadTask(with:fromFile:), which streams from disk and works for background sessions (Q322). Set Content-Length implicitly (URLSession does) and do not set Content-Type on the part unless you know the type. Escape quotes in filenames, and use Content-Disposition filename* or encode carefully for non-ASCII names. Test against the real server: tiny differences (a missing final CRLF, \n instead of CRLF, a missing blank line) are the cause of most “400 bad request” upload bugs.
Each part is boundary, headers, blank line, content; the closing delimiter ends with two dashes.
Common traps
- Using
\ninstead of\r\n. Strict servers reject the body. - Loading a large video into
Datato build the body. Write it to a file and stream it.
They might follow up with
- Why a random boundary? If the data contained the boundary text the server would cut the part short.
- How do I show progress? A delegate’s
didSendBodyDataor the task’sprogressproperty (Q322).
Related questions
- 12.9.2 How do you read a response as a stream with URLSession.bytes, and why does lines skip blank lines when you…
- 12.9.4 How does HMAC request signing work, what must a canonical string include, and how do you verify a signature…
- 12.4.2 How do you do multipart uploads, large uploads and downloads, and show progress?
- 12.3.1 How do you decode and paginate an API response, and which pagination style is safest?
← How do you read a response as a stream with URLSession.bytes, and why… How does HMAC request signing work, what must a canonical string… →