Swift URLSession Status and Response Validation

Separate transport failure, HTTP rejection, decoding and domain validation, and test a network boundary without relying on a live service.

Lesson 38 of 43Advanced9 min read

A successful network transfer is not necessarily a successful application response. A server can return an HTTP error with a perfectly readable body, or return a success status whose payload does not match the expected model. A robust client checks the layers in order and preserves useful failure information.

The concurrency lesson introduces an async URLSession call. This reference develops the HTTP contract around it. Async data APIs require supported Foundation versions, including macOS 12 or iOS 15 and later on Apple platforms. The deterministic examples use fixtures rather than claim a public service always returns one output.

Separate the response layers

Transport failure includes conditions such as no connection, a cancelled request or a timeout. URLSession can throw before a usable response exists. An HTTP response with a status such as 404 is a different outcome: transport may have succeeded, but the server rejected or could not fulfill the request.

After status validation, decoding checks whether the body fits the agreed representation. Domain validation then checks meaning, such as a nonblank code or a permitted quantity. Keep authentication and authorization in the server contract as well; successfully decoding a record does not establish that the caller may change it.

Define accepted statuses according to the endpoint. A JSON resource request might accept a specific success range, while a no-content update must not be decoded as a required object. Content type and payload size can also be part of the contract. Do not apply one generic decoder to every endpoint regardless of its documented success behavior.

Requests, secrets and cancellation need policy

Use URLComponents when building query parameters from data so reserved characters are encoded correctly. Choose the method, headers and body according to the endpoint. Do not put credentials in a committed URL, print authorization headers, or log whole personal payloads merely to diagnose a failed decode.

Timeout and retry behavior should follow the operation's semantics. Retrying an idempotent read differs from repeating a payment submission that may already have completed. A lost response does not prove the server performed no effect. Use idempotency keys or a reconciliation contract when the service supports them.

Cancellation should remain visible as cancellation rather than be turned into a successful empty list. URLSession cancellation may surface through its documented error path, and task ownership should ensure a disappeared screen does not publish obsolete data. Keep updates to UI-owned state under the correct actor isolation rather than infer that an async call returns on a particular thread.

Inject the transport for deterministic tests

Separate obtaining bytes from interpreting them. A transport interface can return Data and URLResponse, while a client applies the endpoint's validation. A fake transport supplies a known success, rejection or malformed payload without external connectivity, credentials or service drift.

Test a 200 valid body, a rejected status, malformed JSON and a semantically invalid model as separate cases. Add transport failure and cancellation cases at the async boundary. Do not assert the exact localized text of a system error when a stable error category or domain result is what callers need.

Whole-body data requests retain their payload in memory. For large downloads or streams, choose the appropriate URLSession API and a deliberate size policy. This small-client example is not a claim that every response can safely be materialized and decoded on a latency-sensitive UI path.

A response without a body needs its own success representation. For example, an endpoint documenting no-content success should not fail merely because a client tries to decode a mandatory JSON object from empty bytes. Conversely, an HTML error page with an unexpected media type should not be mistaken for the endpoint's normal payload. Keep these representation choices beside status validation in the endpoint-specific client contract.

Official references

Read URLSession and async data requests). The Swift concurrency guide explains task ownership and isolation. Review endpoint statuses, payload limits, credential handling, cancellation and stale-result publication before describing a client as robust.

Swift URLSession Status and Response Validation Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Validate a fixture response before decoding

This creates a response object and local bytes; it makes no network request. The known URL and fixture response are valid constants. A real client should validate dynamic URL construction and preserve decoding errors instead of hiding them behind a single generic message.

Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

struct Product: Decodable { let code: String; let quantity: Int }
enum ClientError: Error { case nonHTTP, status(Int), invalidProduct }

func decodeProduct(data: Data, response: URLResponse) throws -> Product {
    guard let http = response as? HTTPURLResponse else { throw ClientError.nonHTTP }
    guard (200...299).contains(http.statusCode) else { throw ClientError.status(http.statusCode) }
    let product = try JSONDecoder().decode(Product.self, from: data)
    guard !product.code.isEmpty, product.quantity >= 0 else { throw ClientError.invalidProduct }
    return product
}

let url = URL(string: "https://example.com/products/PEN")!
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
let data = Data(#"{"code":"PEN","quantity":3}"#.utf8)
do {
    let product = try decodeProduct(data: data, response: response)
    print("\(product.code):\(product.quantity)")
} catch { print("response rejected") }
Output
PEN:3

Swift URLSession Status and Response Validation in Real Projects

Worked exercise: Use a fake async transport

Build a client that accepts an async transport and validates status before decoding. The real transport implementation shows the URLSession call, but the executable uses the fake, so its expected output is stable and requires no network.

Swift
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

struct Stock: Decodable { let quantity: Int }
enum LoadError: Error { case invalidResponse, invalidStock }
protocol Transport {
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
}
struct SessionTransport: Transport {
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        try await URLSession.shared.data(for: request)
    }
}
struct FakeTransport: Transport {
    func data(for request: URLRequest) async throws -> (Data, URLResponse) {
        let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
        return (Data(#"{"quantity":4}"#.utf8), response)
    }
}
func loadStock(from url: URL, transport: any Transport) async throws -> Stock {
    let (data, response) = try await transport.data(for: URLRequest(url: url))
    guard let http = response as? HTTPURLResponse,
          (200...299).contains(http.statusCode) else { throw LoadError.invalidResponse }
    let stock = try JSONDecoder().decode(Stock.self, from: data)
    guard stock.quantity >= 0 else { throw LoadError.invalidStock }
    return stock
}

@main struct Demo {
    static func main() async {
        do {
            let stock = try await loadStock(from: URL(string: "https://example.com/stock")!, transport: FakeTransport())
            print(stock.quantity)
        } catch { print("load failed") }
    }
}
Output
4

Compile this standalone async entry point with swiftc -parse-as-library Demo.swift on a supported toolchain. The fake's unwraps are fixture assumptions. For a reusable concurrent client, review Sendable and actor isolation as an additional contract rather than assume this teaching transport is a fully production-ready shared service.

Swift URLSession Status and Response Validation FAQ

Does URLSession throw automatically for every 404?

An HTTP rejection is a valid response at the transport layer, so inspect HTTPURLResponse.statusCode explicitly according to the endpoint contract. A readable body can contain an error representation rather than the requested model. Validate the status before success decoding and map known server errors deliberately. Network connectivity, HTTP semantics and JSON shape are distinct layers; treating all readable bytes as success can publish misleading data or hide the server's actual failure.

Should I retry every failed request?

No. Consider whether the operation is idempotent and whether the server may already have performed its effect. A timeout after submitting a payment does not prove that no payment occurred. Use service-supported idempotency or reconciliation policies for such operations. Reads can often support bounded retries, but even then define delay, cancellation and total timeout behavior. An unlimited automatic retry loop can keep work alive beyond its owner's useful lifetime.

Why test with a fake transport?

A fake makes input status, payload and failure timing controllable without relying on connectivity or a changing server. It isolates the client's interpretation contract and lets tests cover malformed JSON and domain boundaries cheaply. Keep separate integration checks for the real service when authorized and appropriate. A deterministic fixture does not prove the external server always behaves correctly; it proves how your client responds to the specific contract cases you supplied.

Can async networking update UI state from anywhere?

UI-owned state still requires the appropriate actor isolation, commonly MainActor. Async syntax does not grant arbitrary cross-actor mutation or promise a specific thread at every point. Keep fetching and response interpretation separate from publishing state, and ensure cancelled or outdated work does not overwrite newer results. The original concurrency concepts remain relevant after HTTP validation; a valid response can still arrive too late for the screen that requested it.

Swift URLSession Status and Response Validation Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

Which test setup isolates response validation from live service variability?

Swift URLSession Status and Response Validation Quiz

2 questions to check what you learned.

  1. What should happen before decoding an HTTP body as a success model?

    • Check that the response and status match the endpoint's accepted contract.
    • Log every authorization header.
    • Assume readable bytes always mean success.
  2. Why can retrying a submitted payment be unsafe?

    • Decoding a response prevents duplicate requests.
    • The server may have performed the effect even if its response was lost.
    • All HTTP requests are automatically reversible.