How do you do multipart uploads, large uploads and downloads, and show progress?
Predict first
How many boundary markers does a body with one field and one file contain?
import Foundation
func multipart(boundary: String, fields: [String: String], filename: String, data: Data) -> Data {
var b = Data()
let nl = "\r\n"
for (k, v) in fields.sorted(by: { $0.key < $1.key }) {
b.append("--\(boundary)\(nl)Content-Disposition: form-data; name=\"\(k)\"\(nl)\(nl)\(v)\(nl)".data(using: .utf8)!)
}
b.append("--\(boundary)\(nl)Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\(nl)Content-Type: application/octet-stream\(nl)\(nl)".data(using: .utf8)!)
b.append(data)
b.append("\(nl)--\(boundary)--\(nl)".data(using: .utf8)!)
return b
}
let body = multipart(boundary: "B", fields: ["a": "1"], filename: "x.bin", data: Data([1, 2, 3]))
print(String(decoding: body, as: UTF8.self).components(separatedBy: "--B").count - 1)
- 2
- 3
- 4
Mental model. A multipart body is a parcel with labelled compartments separated by a divider card (the boundary). A big parcel goes on a trolley (a file or stream), not in your arms (memory).
Multipart form data sends several parts (fields and files) in one request with Content-Type: multipart/form-data; boundary=B. The body is plain text framing: for each part, a line --B, headers (Content-Disposition: form-data; name="file"; filename="a.png" and a Content-Type), a blank line, the bytes, and a CRLF; the body ends with --B--. Lines end with \r\n, the boundary must not occur inside the data (use a random UUID), and the field names are the server’s contract. Large bodies must not be built in memory: write the multipart body to a temporary file and use upload(for:fromFile:), or provide a stream (httpBodyStream) so the memory use stays flat; downloads use download(for:), which streams to a file you must move out of the temporary location before the call returns, and bytes(for:) when you want to process data incrementally as it arrives.
Progress: every task exposes a Progress object (task.progress, with fractionCompleted) that you can observe with KVO or bind to a ProgressView; the delegate callbacks urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:) and urlSession(_:downloadTask:didWriteData:…) give raw byte counts; and the async APIs take a delegate: parameter for per-task progress. Make large transfers resumable: for downloads keep the resumeData from a failed or cancelled task and start again with downloadTask(withResumeData:) (it needs the server to support range requests and a stable ETag), and for uploads use a chunked or resumable protocol (tus, or the server’s own). Use a background session for anything the user expects to finish after leaving the app, and watch the user’s data constraints (allowsExpensiveNetworkAccess, allowsConstrainedNetworkAccess). Validate size and type before uploading, and compress images to a sensible size first.
One opening marker per part (two) plus the closing marker --B-- makes three.
Common traps
- Building a large multipart body in memory. It can exhaust memory; write it to a file and upload from the file.
- Using
\ninstead of\r\nin the framing. Many servers reject the body.
They might follow up with
- How do you resume an interrupted download? Keep
resumeDataand start a new task with it. - Where does a finished download live? A temporary file, which you must move before the delegate callback or call returns.
Related questions
- 12.1.1 What do the URLSession configurations control, and how do default, ephemeral and background sessions differ?
- 12.1.2 How do you make a request with URLSession, and why is a 404 not an error?
- 6.5.1 An app goes to the background. What can it still do, and how do you choose a mechanism?
← How does URLSessionWebSocketTask work, and what do you have to handle… Why use NWPathMonitor instead of reachability, and how do you use… →