# URLSession Patterns

Longer implementations behind the summaries in [SKILL.md](../SKILL.md). The
types here are the same ones SKILL.md names: `APIClientProtocol` with
`fetch`, `send`, `perform` and `upload`; `Endpoint`; `NetworkError`;
`RequestMiddleware`. None of the code force-unwraps.

## Contents

- [A protocol-backed API client](#a-protocol-backed-api-client)
- [Building requests step by step](#building-requests-step-by-step)
- [Uploading multipart form data](#uploading-multipart-form-data)
- [Downloads that report progress](#downloads-that-report-progress)
- [Paging by cursor](#paging-by-cursor)
- [Paging by offset](#paging-by-offset)
- [Stubbing the network with URLProtocol](#stubbing-the-network-with-urlprotocol)
- [Retries that back off exponentially](#retries-that-back-off-exponentially)
- [Pinning certificates in a session delegate](#pinning-certificates-in-a-session-delegate)
- [Middleware that logs requests](#middleware-that-logs-requests)
- [Choosing a cache policy](#choosing-a-cache-policy)
- [Reading a server-sent event stream](#reading-a-server-sent-event-stream)
- [A production-ready session setup](#a-production-ready-session-setup)

## A protocol-backed API client

### Protocol

```swift
protocol APIClientProtocol: Sendable {
    func fetch<Reply: Decodable & Sendable>(_ endpoint: Endpoint, as kind: Reply.Type) async throws -> Reply
    func send<Reply: Decodable & Sendable>(_ endpoint: Endpoint, json payload: some Encodable & Sendable,
                                           as kind: Reply.Type) async throws -> Reply
    func perform(_ endpoint: Endpoint) async throws
    func upload<Reply: Decodable & Sendable>(_ endpoint: Endpoint, bytes: Data,
                                             as kind: Reply.Type) async throws -> Reply
}
```

- `fetch` decodes the response of any request that carries no Swift body.
- `send` JSON-encodes a body, then decodes the reply.
- `perform` validates the status and ignores the body (DELETE, fire-and-ack).
- `upload` sends raw bytes through `upload(for:from:)`.

### Endpoint Definition

`Endpoint` is exactly the struct shown in SKILL.md: `path`, a nested
`HTTPMethod` enum (get, post, put, patch, delete, sent upper-cased), query items,
headers, an optional `body`, a `cachePolicy` defaulting to
`.useProtocolCachePolicy` and a 30 second `timeoutInterval`. Its
`urlRequest(relativeTo:)` adds query items only when there are some, copies
method, body, cache policy, timeout and every header, and throws
`NetworkError.invalidURL` when components or the final URL cannot be formed.

### Client Implementation

```swift
final class APIClient: APIClientProtocol {
    private let root: URL
    private let transport: URLSession
    private let pipeline: [any RequestMiddleware]
    private let decoder = JSONDecoder.snakeCaseISO8601
    private let encoder = JSONEncoder.snakeCaseISO8601

    init(baseURL: URL, session: URLSession, middleware: [any RequestMiddleware] = []) {
        root = baseURL
        transport = session
        pipeline = middleware
    }

    func fetch<Reply: Decodable & Sendable>(_ endpoint: Endpoint, as kind: Reply.Type) async throws -> Reply {
        let outgoing = try await prepared(endpoint)
        return try decoded(kind, try await execute { try await self.transport.data(for: outgoing) })
    }

    func send<Reply: Decodable & Sendable>(_ endpoint: Endpoint, json payload: some Encodable & Sendable,
                                           as kind: Reply.Type) async throws -> Reply {
        var json = endpoint
        json.body = try encoder.encode(payload)
        json.headers["Content-Type"] = "application/json"
        return try await fetch(json, as: kind)
    }

    func perform(_ endpoint: Endpoint) async throws {
        let outgoing = try await prepared(endpoint)
        _ = try await execute { try await self.transport.data(for: outgoing) }
    }

    func upload<Reply: Decodable & Sendable>(_ endpoint: Endpoint, bytes: Data,
                                             as kind: Reply.Type) async throws -> Reply {
        let outgoing = try await prepared(endpoint)
        return try decoded(kind, try await execute { try await self.transport.upload(for: outgoing, from: bytes) })
    }

    private func prepared(_ endpoint: Endpoint) async throws -> URLRequest {
        var outgoing = try endpoint.urlRequest(relativeTo: root)
        for step in pipeline { try await step.adapt(&outgoing) }
        return outgoing
    }

    private func execute(_ call: () async throws -> (Data, URLResponse)) async throws -> Data {
        let data: Data
        let response: URLResponse
        do {
            (data, response) = try await call()
        } catch let error as URLError {
            throw NetworkError.from(error)
        }
        guard let status = (response as? HTTPURLResponse)?.statusCode else {
            throw NetworkError.invalidResponse
        }
        if !(200...299).contains(status) {
            throw NetworkError.httpStatus(code: status, body: data,
                                          message: APIErrorBody.decode(from: data)?.message)
        }
        return data
    }

    private func decoded<Reply: Decodable>(_ kind: Reply.Type, _ payload: Data) throws -> Reply {
        do { return try decoder.decode(kind, from: payload) }
        catch { throw NetworkError.decodingFailed(error) }
    }
}

extension JSONDecoder {
    static var snakeCaseISO8601: JSONDecoder {
        let made = JSONDecoder()
        made.keyDecodingStrategy = .convertFromSnakeCase
        made.dateDecodingStrategy = .iso8601
        return made
    }
}

extension JSONEncoder {
    static var snakeCaseISO8601: JSONEncoder {
        let made = JSONEncoder()
        made.keyEncodingStrategy = .convertToSnakeCase
        made.dateEncodingStrategy = .iso8601
        return made
    }
}
```

Middleware runs in array order, so put authentication before logging if the
log should show that a header was added. Thin conveniences keep call sites
short:

```swift
extension APIClient {
    func get<Reply: Decodable & Sendable>(_ route: String, query: [URLQueryItem] = [],
                                          as kind: Reply.Type) async throws -> Reply {
        try await fetch(Endpoint(path: route, queryItems: query), as: kind)
    }

    func post<Reply: Decodable & Sendable>(_ route: String, json payload: some Encodable & Sendable,
                                           as kind: Reply.Type) async throws -> Reply {
        try await send(Endpoint(path: route, method: .post), json: payload, as: kind)
    }

    func delete(_ route: String) async throws {
        try await perform(Endpoint(path: route, method: .delete))
    }
}
```

### Error Types

```swift
enum NetworkError: Error, LocalizedError {
    case invalidURL
    case invalidResponse
    case httpStatus(code: Int, body: Data, message: String?)
    case decodingFailed(any Error)
    case noConnection
    case timedOut
    case cancelled
    case transport(URLError)

    static func from(_ error: URLError) -> NetworkError {
        switch error.code {
        case .notConnectedToInternet, .networkConnectionLost: .noConnection
        case .timedOut: .timedOut
        case .cancelled: .cancelled
        default: .transport(error)
        }
    }

    var errorDescription: String? {
        switch self {
        case .invalidURL: "The request address is not valid."
        case .invalidResponse: "The server sent a response that could not be read."
        case .httpStatus(let status, _, let message): message ?? "The server returned status \(status)."
        case .decodingFailed: "The server data was in an unexpected format."
        case .noConnection: "You appear to be offline."
        case .timedOut: "The server took too long to answer."
        case .cancelled: nil
        case .transport(let error): error.localizedDescription
        }
    }
}

struct APIErrorBody: Decodable, Sendable {
    let message: String?
    let code: String?

    static func decode(from data: Data) -> APIErrorBody? {
        try? JSONDecoder().decode(APIErrorBody.self, from: data)
    }
}
```

`.cancelled` has no description on purpose: a cancelled request should not
produce an alert.

### Request Middleware

`RequestMiddleware` and `BearerTokenMiddleware` are the ones in SKILL.md:
one `adapt(_:)` method that edits the request in place, and a middleware that asks an async token provider
for the current token and sets `Authorization: Bearer ...`.

## Building requests step by step

For requests with many optional parts, a value-type builder keeps call sites
readable and hard to get wrong. Each modifier returns a changed copy.

```swift
struct RequestBuilder: Sendable {
    private let target: URL
    private var verb = "GET"
    private var caching = URLRequest.CachePolicy.useProtocolCachePolicy
    private var limit: TimeInterval = 30
    private var items: [URLQueryItem] = []
    private var fields: [String: String] = [:]
    private var payload: Data?

    init(baseURL: URL, path: String) {
        target = baseURL.appending(path: path)
    }

    private func with(_ change: (inout Self) throws -> Void) rethrows -> Self {
        var next = self
        try change(&next)
        return next
    }

    func method(_ verb: String) -> Self { with { $0.verb = verb } }
    func timeout(_ seconds: TimeInterval) -> Self { with { $0.limit = seconds } }
    func cachePolicy(_ rule: URLRequest.CachePolicy) -> Self { with { $0.caching = rule } }
    func header(_ field: String, _ text: String) -> Self { with { $0.fields[field] = text } }

    func query(_ key: String, _ text: String?) -> Self {
        guard let text else { return self }
        return with { $0.items.append(URLQueryItem(name: key, value: text)) }
    }

    func jsonBody(_ model: some Encodable) throws -> Self {
        let encoded = try JSONEncoder().encode(model)
        return with {
            $0.payload = encoded
            $0.fields["Content-Type"] = "application/json"
        }
    }

    func build() throws -> URLRequest {
        var parts = URLComponents(url: target, resolvingAgainstBaseURL: true)
        if !items.isEmpty { parts?.queryItems = items }
        guard let url = parts?.url else { throw NetworkError.invalidURL }
        var built = URLRequest(url: url, cachePolicy: caching, timeoutInterval: limit)
        built.httpMethod = verb
        built.httpBody = payload
        built.allHTTPHeaderFields = fields
        return built
    }
}

let request = try RequestBuilder(baseURL: apiRoot, path: "tickets")
    .method("POST")
    .header("X-Correlation-ID", UUID().uuidString)
    .jsonBody(TicketDraft(subject: "Printer offline"))
    .timeout(15)
    .build()
```

`query(_:_:)` quietly drops a `nil` value, which keeps optional filters tidy.

## Uploading multipart form data

`multipart/form-data` is plain text framing around each part; you can build
it without a library.

```swift
struct MultipartForm: Sendable {
    private struct Section: Sendable { let lines: [String]; let payload: Data }

    let separator = "Boundary-\(UUID().uuidString)"
    private var sections: [Section] = []

    var headerValue: String { "multipart/form-data; boundary=\(separator)" }

    mutating func addText(_ value: String, named field: String) {
        let disposition = #"Content-Disposition: form-data; name="\#(field)""#
        sections.append(Section(lines: [disposition], payload: Data(value.utf8)))
    }

    mutating func addFile(_ data: Data, named field: String, filename file: String, type mimeType: String) {
        let disposition = #"Content-Disposition: form-data; name="\#(field)"; filename="\#(file)""#
        sections.append(Section(lines: [disposition, "Content-Type: " + mimeType], payload: data))
    }

    func encoded() -> Data {
        var out = Data()
        for section in sections {
            out.append(line: "--\(separator)")
            section.lines.forEach { out.append(line: $0) }
            out.append(line: "")
            out.append(section.payload)
            out.append(line: "")
        }
        out.append(line: "--\(separator)--")
        return out
    }
}

extension Data {
    mutating func append(line: String) { append(Data((line + "\r\n").utf8)) }
}

var form = MultipartForm()
form.addText("Harbor at dusk", named: "caption")
form.addFile(jpegData, named: "photo", filename: "harbor.jpg", type: "image/jpeg")
var post = URLRequest(url: uploadURL)
post.httpMethod = "POST"
post.allHTTPHeaderFields = ["Content-Type": form.headerValue]
let (reply, _) = try await session.upload(for: post, from: form.encoded())
```

`Data(string.utf8)` never fails, so there is no optional to unwrap. For a
background upload, write `encoded()` to a file and upload the file (see
[background-websocket.md](background-websocket.md#uploads-from-a-file-on-disk)).

## Downloads that report progress

`bytes(for:)` and `bytes(from:)` (iOS 15 and later) expose the body byte by
byte, and `response.expectedContentLength` gives the total when the server
sends one:

```swift
func downloadReportingProgress(from url: URL, onFraction: (Double) -> Void) async throws -> Data {
    let (incoming, response) = try await session.bytes(from: url)
    let expected = response.expectedContentLength
    var buffer = Data()
    if expected > 0 { buffer.reserveCapacity(Int(expected)) }
    for try await octet in incoming {
        buffer.append(octet)
        if expected > 0, buffer.count % 65_536 == 0 {
            onFraction(Double(buffer.count) / Double(expected))
        }
    }
    return buffer
}
```

This holds the whole body in memory. For big files use a download task with a
delegate, which writes to disk and also works in background sessions.

### Delegate-Based File Download with Progress

A delegate object can turn callbacks into an `AsyncStream`:

```swift
enum TransferEvent: Sendable {
    case progress(Double)
    case finished(URL)
    case failed(any Error)
}

final class FileDownload: NSObject, URLSessionDownloadDelegate, Sendable {
    private let continuation: AsyncStream<TransferEvent>.Continuation

    private init(continuation: AsyncStream<TransferEvent>.Continuation) {
        self.continuation = continuation
    }

    static func start(from url: URL) -> AsyncStream<TransferEvent> {
        let (stream, continuation) = AsyncStream.makeStream(of: TransferEvent.self)
        let observer = FileDownload(continuation: continuation)
        let owner = URLSession(configuration: .default, delegate: observer, delegateQueue: nil)
        owner.downloadTask(with: url).resume()
        continuation.onTermination = { _ in owner.invalidateAndCancel() }
        return stream
    }

    nonisolated func urlSession(_: URLSession, downloadTask _: URLSessionDownloadTask,
                                didFinishDownloadingTo location: URL) {
        let kept = URL.temporaryDirectory.appending(path: UUID().uuidString)
        do {
            let files = FileManager.default
            try files.moveItem(at: location, to: kept)
            continuation.yield(.finished(kept))
        } catch {
            continuation.yield(.failed(error))
        }
        continuation.finish()
    }

    nonisolated func urlSession(_: URLSession, downloadTask _: URLSessionDownloadTask,
                                didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
                                totalBytesExpectedToWrite: Int64) {
        guard totalBytesExpectedToWrite > 0 else { return }
        continuation.yield(.progress(Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)))
    }

    nonisolated func urlSession(_: URLSession, task _: URLSessionTask,
                                didCompleteWithError error: (any Error)?) {
        guard let error else { return }
        continuation.yield(.failed(error))
        continuation.finish()
    }
}
```

The move happens inside `didFinishDownloadingTo` because the system deletes
`location` as soon as that method returns. A session retains its delegate
until it is invalidated, so the stream's termination handler invalidates it.

## Paging by cursor

```swift
struct CursorPage<Item: Decodable & Sendable>: Decodable, Sendable {
    struct Paging: Decodable, Sendable {
        let nextCursor: String?
        let hasMore: Bool
    }
    let data: [Item]
    let pagination: Paging
}

struct CursorPager<Item: Decodable & Sendable>: AsyncSequence {
    typealias Element = [Item]
    let loadPage: @Sendable (_ cursor: String?) async throws -> CursorPage<Item>

    struct AsyncIterator: AsyncIteratorProtocol {
        let loadPage: @Sendable (String?) async throws -> CursorPage<Item>
        var cursor: String?
        var finished = false

        mutating func next() async throws -> [Item]? {
            guard !finished else { return nil }
            try Task.checkCancellation()
            let page = try await loadPage(cursor)
            cursor = page.pagination.nextCursor
            finished = !page.pagination.hasMore
            return page.data.isEmpty ? nil : page.data
        }
    }

    func makeAsyncIterator() -> AsyncIterator { AsyncIterator(loadPage: loadPage) }
}

let pager = CursorPager<Comment> { cursor in
    let query = [URLQueryItem(name: "limit", value: "50")]
        + (cursor.map { [URLQueryItem(name: "cursor", value: $0)] } ?? [])
    return try await client.fetch(Endpoint(path: "comments", queryItems: query),
                                  as: CursorPage<Comment>.self)
}
var comments: [Comment] = []
for try await batch in pager { comments += batch }
```

## Paging by offset

```swift
struct OffsetPager<Item: Sendable>: AsyncSequence {
    typealias Element = [Item]
    var pageSize = 20
    let loadPage: @Sendable (_ skip: Int, _ take: Int) async throws -> [Item]

    struct AsyncIterator: AsyncIteratorProtocol {
        let pageSize: Int
        let loadPage: @Sendable (Int, Int) async throws -> [Item]
        var offset = 0
        var finished = false

        mutating func next() async throws -> [Item]? {
            guard !finished else { return nil }
            try Task.checkCancellation()
            let items = try await loadPage(offset, pageSize)
            offset += items.count
            finished = items.count < pageSize
            return items.isEmpty ? nil : items
        }
    }

    func makeAsyncIterator() -> AsyncIterator { AsyncIterator(pageSize: pageSize, loadPage: loadPage) }
}
```

A short page means the server has nothing more, so the next call ends the
sequence without another request.

## Stubbing the network with URLProtocol

A `URLProtocol` subclass intercepts requests at the transport layer. It works
with any session configuration and needs no change to production code, so
it tests the real client, middleware and validation.

```swift
final class StubURLProtocol: URLProtocol {
    nonisolated(unsafe) static var respond: ((URLRequest) throws -> (HTTPURLResponse, Data))?

    override class func canInit(with _: URLRequest) -> Bool { true }
    override class func canonicalRequest(for original: URLRequest) -> URLRequest { original }

    override func startLoading() {
        guard let respond = Self.respond else {
            client?.urlProtocol(self, didFailWithError: URLError(.resourceUnavailable))
            return
        }
        do {
            let (head, payload) = try respond(request)
            client?.urlProtocol(self, didReceive: head, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: payload)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }

    override func stopLoading() {}
}
```

A missing stub fails the request instead of crashing the test run.

### Test Setup

```swift
import Testing
import Foundation

@Suite(.serialized)
struct APIClientTests {
    func makeClient(middleware: [any RequestMiddleware] = []) throws -> APIClient {
        let config = URLSessionConfiguration.ephemeral
        config.protocolClasses = [StubURLProtocol.self]
        return APIClient(baseURL: try #require(URL(string: "https://api.test")),
                         session: URLSession(configuration: config), middleware: middleware)
    }

    func reply(_ status: Int, _ json: String, for request: URLRequest) throws -> (HTTPURLResponse, Data) {
        let url = try #require(request.url)
        let response = try #require(HTTPURLResponse(url: url, statusCode: status, httpVersion: nil, headerFields: nil))
        return (response, Data(json.utf8))
    }

    @Test func decodesProjects() async throws {
        StubURLProtocol.respond = { request in
            #expect(request.url?.path() == "/projects")
            return try self.reply(200, #"[{"id":7,"title":"Atlas"},{"id":8,"title":"Beacon"}]"#, for: request)
        }
        let projects = try await makeClient().fetch(Endpoint(path: "projects"), as: [Project].self)
        #expect(projects.count == 2)
    }

    @Test func notFoundThrows() async throws {
        StubURLProtocol.respond = { try self.reply(404, "{}", for: $0) }
        let client = try makeClient()
        await #expect(throws: NetworkError.self) {
            try await client.perform(Endpoint(path: "projects/99"))
        }
    }

    @Test func middlewareSignsRequests() async throws {
        StubURLProtocol.respond = { request in
            #expect(request.allHTTPHeaderFields?["Authorization"] == "Bearer fixture-token")
            return try self.reply(200, "{}", for: request)
        }
        let client = try makeClient(middleware: [BearerTokenMiddleware { "fixture-token" }])
        _ = try await client.fetch(Endpoint(path: "me"), as: Empty.self)
    }
}

struct Project: Decodable, Sendable { let id: Int; let title: String }
struct Empty: Decodable, Sendable {}
```

The suite is serialized because the stub is shared static state. `Empty`
decodes a `{}` body when only the status matters.

## Retries that back off exponentially

Three rules: stop on cancellation, do not retry a 4xx other than 429, and
add jitter so that many clients do not retry in lockstep after an outage.
`withRetry` in SKILL.md implements this with a `maxDelay` cap, a
`shouldRetry` predicate, `Task.checkCancellation()` before every attempt, and
up to 10% random jitter on the capped delay. It throws immediately when the
predicate says no or on the final attempt. The default predicate:

```swift
func isTransient(_ error: any Error) -> Bool {
    switch error {
    case is CancellationError:
        return false
    case let failure as NetworkError:
        switch failure {
        case .httpStatus(let status, _, _): return status >= 500 || status == 429
        case .timedOut, .noConnection: return true
        case .transport(let urlError): return urlError.code == .networkConnectionLost
        default: return false
        }
    case let urlError as URLError:
        let transient: Set<URLError.Code> = [.timedOut, .networkConnectionLost, .notConnectedToInternet]
        return transient.contains(urlError.code)
    default:
        return false
    }
}

let summary = try await withRetry(maxAttempts: 4) {
    try await client.fetch(Endpoint(path: "summary"), as: Summary.self)
}
```

The raw `URLError` branch covers callers that talk to URLSession directly
instead of through `APIClient`.

## Pinning certificates in a session delegate

- When the pins can ship in `Info.plist`, use ATS `NSPinnedDomains` instead
  of writing trust code.
- Hand-written trust evaluation in `urlSession(_:didReceive:completionHandler:)`
  belongs to `swift-security`. SPKI pinning hashes the DER-encoded Subject
  Public Key Info; the bytes from `SecKeyCopyExternalRepresentation` alone are
  not that.
- Always pin at least two keys, the live one and a backup, so a rotation
  cannot lock users out.
- Keep a remote switch (a feature flag) that can turn pinning off in an
  emergency.
- Try a full key rotation against a staging host first, never first in production.
- Run the system trust evaluation first; pins only narrow an already trusted
  chain.
- Keep the trust implementation inside the security module rather than
  scattering it through networking code.

## Middleware that logs requests

Log traffic while developing, and compile it out or turn it down for
release.

```swift
import OSLog

struct DebugLogMiddleware: RequestMiddleware {
    private let log = Logger(subsystem: "app.networking", category: "http")

    func adapt(_ request: inout URLRequest) async throws {
        #if DEBUG
        let verb = request.httpMethod ?? "GET"
        log.debug("\(verb) \(request.url?.absoluteString ?? "-")")
        let visible = (request.allHTTPHeaderFields ?? [:]).filter { $0.key != "Authorization" }
        visible.forEach { log.debug("  \($0.key): \($0.value)") }
        if let payload = request.httpBody, payload.count < 10_000 {
            log.debug("  body: \(String(decoding: payload, as: UTF8.self))")
        }
        #endif
    }
}
```

The `Authorization` header is never written out, and large bodies are
skipped.

### Response Logging

Middleware sees only the outgoing request. To log outcomes and timing, wrap
the transport call:

```swift
func timed(_ outgoing: URLRequest, on session: URLSession, log: Logger) async throws -> (Data, URLResponse) {
    let clock = ContinuousClock()
    let start = clock.now
    let route = outgoing.url?.path() ?? "-"
    do {
        let result = try await session.data(for: outgoing)
        let status = (result.1 as? HTTPURLResponse)?.statusCode ?? -1
        log.debug("\(status) \(route) in \(clock.now - start)")
        return result
    } catch {
        log.error("failed \(route) after \(clock.now - start): \(error.localizedDescription)")
        throw error
    }
}
```

## Choosing a cache policy

### URLCache Configuration

```swift
let cacheFolder = URL.cachesDirectory.appending(path: "HTTPCache")
let megabyte = 1_048_576
let cache = URLCache(memoryCapacity: 50 * megabyte, diskCapacity: 200 * megabyte, directory: cacheFolder)
let config = URLSessionConfiguration.default
config.urlCache = cache
config.requestCachePolicy = .returnCacheDataElseLoad
```

### Per-Request Cache Control

| Policy | Behaviour |
|---|---|
| `.reloadIgnoringLocalCacheData` | Always go to the network |
| `.returnCacheDataElseLoad` | Use a cached response if there is one |
| `.returnCacheDataDontLoad` | Cache only; an offline mode |

### ETag / If-None-Match

```swift
func fetchRevalidating<Model: Decodable>(_ kind: Model.Type, at address: URL,
                                         stored: (etag: String, body: Data)?) async throws -> (Model, String?) {
    var conditional = URLRequest(url: address)
    if let stored { conditional.addValue(stored.etag, forHTTPHeaderField: "If-None-Match") }
    let (fresh, reply) = try await session.data(for: conditional)
    let head = reply as? HTTPURLResponse
    if head?.statusCode == 304, let stored {
        return (try JSONDecoder().decode(kind, from: stored.body), stored.etag)
    }
    return (try JSONDecoder().decode(kind, from: fresh), head?.value(forHTTPHeaderField: "ETag"))
}
```

A 304 means the stored copy is still current, so decode it and keep its tag;
otherwise decode the fresh body and store the new tag.

## Reading a server-sent event stream

```swift
struct StreamEvent: Sendable {
    var name: String?
    var data: String
    var id: String?
}

func eventStream(for request: URLRequest, on session: URLSession) -> AsyncThrowingStream<StreamEvent, any Error> {
    var accepting = request
    accepting.addValue("text/event-stream", forHTTPHeaderField: "Accept")
    let streaming = accepting
    return AsyncThrowingStream { continuation in
        let reader = Task {
            do {
                let (feed, _) = try await session.bytes(for: streaming)
                var pending = StreamEvent(data: "")
                var dataLines: [String] = []
                for try await row in feed.lines {
                    if row.isEmpty {
                        if !dataLines.isEmpty {
                            pending.data = dataLines.joined(separator: "\n")
                            continuation.yield(pending)
                        }
                        pending = StreamEvent(data: "")
                        dataLines = []
                    } else if let value = field("event", in: row) {
                        pending.name = value
                    } else if let value = field("data", in: row) {
                        dataLines.append(value)
                    } else if let value = field("id", in: row) {
                        pending.id = value
                    }
                }
                continuation.finish()
            } catch {
                continuation.finish(throwing: error)
            }
        }
        continuation.onTermination = { _ in reader.cancel() }
    }
}

private func field(_ name: String, in row: String) -> String? {
    guard row.hasPrefix(name + ":") else { return nil }
    return row.dropFirst(name.count + 1).trimmingCharacters(in: .whitespaces)
}
```

The request is copied into a `let` before the reader `Task` is created: a
captured `var` would be shared with the task, and Swift 6 rejects that as a
possible data race. A blank line ends an event; several `data:` lines join
with newlines.
`AsyncLineSequence` may not hand you empty lines on every OS release, and the
parser depends on them. If events never dispatch, split `bytes` on `\n`
yourself instead of using `lines`, and test against the real endpoint.

## A production-ready session setup

Decide timeouts, caching, connectivity behaviour and delegates once, when the
session is created, and pass that session to clients.

```swift
enum SessionFactory {
    static func standard(observer: (any URLSessionDelegate)? = nil) -> URLSession {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 300
        config.waitsForConnectivity = true
        config.httpMaximumConnectionsPerHost = 6
        config.requestCachePolicy = .useProtocolCachePolicy
        config.httpAdditionalHeaders = [
            "Accept": "application/json",
            "Accept-Encoding": "gzip, deflate, br",
        ]
        let mb = 1_048_576
        config.urlCache = URLCache(memoryCapacity: 25 * mb, diskCapacity: 100 * mb)
        return URLSession(configuration: config, delegate: observer, delegateQueue: nil)
    }
}
```
