How do you make a request with URLSession, and why is a 404 not an error?
Predict first
A server answers 404 with a body. What does data(from:) do? (A stub session answers the request.)
Stub.handler = { _ in (404, [:], Data("nope".utf8)) }
let (data, resp) = try await session().data(from: URL(string: "https://example.test/x")!)
print((resp as! HTTPURLResponse).statusCode, String(data: data, encoding: .utf8)!)
- It throws a URLError
- 404 nope
- 200 nope
Predict first
What are the defaults of a fresh request?
import Foundation
let r = URLRequest(url: URL(string: "https://a.b")!)
print(r.timeoutInterval, r.httpMethod ?? "nil", r.cachePolicy == .useProtocolCachePolicy)
- 60.0 GET true
- 30.0 POST false
- 60.0 nil true
Mental model. A postal service delivers the letter and brings back the reply, even if the reply is “address unknown”. It only reports failure when it could not deliver at all.
The modern call is async: let (data, response) = try await session.data(for: request) (also data(from:), upload(for:from:) and download(for:), which writes to a temporary file). It returns the body and a URLResponse that you cast to HTTPURLResponse for the status code and headers. The older forms are completion-handler tasks (dataTask(with:completionHandler:), which you must resume() or nothing happens) and the delegate API for streaming, progress, authentication challenges and metrics. The async call is cancellable by structure: cancelling the surrounding Task cancels the request and throws URLError(.cancelled).
Understand what counts as failure. The call throws only for transport problems (URLError: no connection, timeout, DNS failure, TLS failure, cancelled). An HTTP error response is a success as far as URLSession is concerned: a 404 or 500 returns normally with the status code and body, so you must check statusCode yourself (typically 200..<300 is success) and decode the error body separately. Build a request with URLRequest: httpMethod (default GET), headers with setValue(_:forHTTPHeaderField:), httpBody for a small body (or an upload from a file or stream for large ones), and timeoutInterval (default 60). Use URLComponents to build query strings so values are percent-encoded. Handle the response in one place, mapping status codes and URLError codes to your own error type, since that is the layer every caller needs.
The call succeeds because the transport worked. The status code is in the response and is yours to check.
A new request is a GET with a 60-second timeout that follows the protocol’s cache rules.
Common traps
- Treating a returned response as success. A 404 or 500 does not throw; check the status code.
- Forgetting
resume()on a completion-handler task. Nothing starts.
They might follow up with
- What does
URLError(.cancelled)mean? The task was cancelled, often by cancelling the surroundingTask; handle it quietly. - How do you send a large file? An upload task from a file or stream, not an in-memory
httpBody.
Related questions
- 12.1.1 What do the URLSession configurations control, and how do default, ephemeral and background sessions differ?
- 12.5.1 How should a client retry failed requests: what to retry, how long to wait, and what idempotency has to do…
- 2.3.4 Why use URLComponents and URLQueryItem, and what do they still not encode for you?
← What do the URLSession configurations control, and how do default…