---
title: 'The Network Client'
source: 'https://academia.sh/en/courses/data-networking-and-concurrency/network-client'
course: 'Data, Networking and Concurrency'
language: en
updated: '2026-10-21T05:03:09+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Separates the distinct roles of the session, the configuration, and the task in a network request'
    - 'States which job each of the data, upload, and download task types suits'
    - "Actually measures whether an operation's result changes when it is repeated or reordered"
    - 'Shows with a measured operation that idempotency solves repetition but not order'
---

# The Network Client

Session configuration and the task types; measuring, in four categories, whether the result of the six operations behind the network layer changes if they are repeated or reordered.

In the previous course (M30/K04) we compared the value a screen shows with the model's value and checked whether the screen had gone stale. This course goes beneath the screen: to the layer where the value arrives, is stored, and is processed. In this layer the question changes. A screen shows the value or it does not. An operation in the network layer is tested by something else: if the same operation is applied twice, or if two operations swap order, does the result come out different?

It is no accident that this question is asked so often on the network. A request can be lost on the way, a response can arrive late, a user can press the same button twice, two responses can arrive in reverse order. None of these raises an error. The state quietly turns into a different value. In this lesson we first get to know the building blocks of a network client: session, configuration, and task. Then we measure six operations with a single core.

The measurement axis is **sensitivity**. We apply an operation once and twice; if the results differ, the operation is **repeat-sensitive**. We apply two operations in two different orders; if the results differ, the operation is **order-sensitive**. If both hold it is **doubly sensitive**, and if neither holds it is **robust**. In the output the four names are written in ASCII: `robust`, `repeat-sensitive`, `order-sensitive`, `doubly-sensitive`.

```swift
// Replay.swift — classifies an operation's sensitivity to repetition and order
struct Trial<State: Equatable & CustomStringConvertible> {
    let label: String
    let once: State
    let twice: State
    let forward: State
    let backward: State

    var sensitivity: String {
        switch (once != twice, forward != backward) {
        case (false, false): return "robust"
        case (true, false): return "repeat-sensitive"
        case (false, true): return "order-sensitive"
        case (true, true): return "doubly-sensitive"
        }
    }

    func report() -> String {
        print("\(label) | once=\(once) twice=\(twice) ab=\(forward) ba=\(backward) -> \(sensitivity)")
        return sensitivity
    }
}

func replay<State>(_ label: String, from start: State, _ a: (State) -> State, then b: (State) -> State) -> Trial<State> {
    Trial(label: label, once: a(start), twice: a(a(start)), forward: b(a(start)), backward: a(b(start)))
}

func countSensitivities(_ results: [String]) {
    for name in ["robust", "repeat-sensitive", "order-sensitive", "doubly-sensitive"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

`Trial` holds four states: `once` is the state after the operation is applied once, `twice` after it is applied twice. `forward` comes from applying `a` first and then `b`; `backward` from applying `b` first and then `a`. `sensitivity` looks only at the two inequalities. `replay` produces these four states from a starting state. **The rule does not see the cause, it sees the inequality:** it does not say why two states differ, only that they do. The lesson says which state is being compared.

## Session and Configuration

A network request needs three separate objects. The **configuration** says how the session will behave: the timeout, the cache, extra headers, whether to wait when there is no network. The **session** is the client that works with this configuration; requests that come out of the same session share the same rules. The **task** is a single transfer: it takes a request and returns a response or an error.

`URLSessionConfiguration.default` uses a persistent cache and cookie store. `.ephemeral` writes none of these to disk; no trace remains when the session ends. The third configuration is the background session, which is covered in the last lesson of this topic.

```swift
import Foundation

let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = 15
configuration.waitsForConnectivity = true
configuration.httpAdditionalHeaders = ["Accept": "application/json"]
let session = URLSession(configuration: configuration)
```

The configuration is read when the session is created. Changing the configuration after the session exists does not affect the session; to change anything, a new session is created. For this reason applications usually create a small number of sessions and share them everywhere.

## Task Types

Three task types divide the work. A **data task** takes the response into memory; it suits small responses. An **upload task** sends the body of the request as separate data or as a file. A **download task** writes the response directly to a file; it is used so that large bodies do not bloat memory. In the asynchronous form, the first two are the `data(for:)` and `upload(for:from:)` calls; both return a `(Data, URLResponse)` pair.

```swift
// profile request — SOURCE
import Foundation

func fetchProfile(session: URLSession) async throws -> Data {
    let request = URLRequest(url: URL(string: "https://api.example.com/profile")!)
    let (data, response) = try await session.data(for: request)
    guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }
    return data
}
```

This snippet is not run; it reaches out to the network. `data(for:)` is an **asynchronous function**: the call waits at a **suspension point** until the response arrives, and the flow then continues from where it left off. Whether the response is a real response is checked separately; the status code is read through `HTTPURLResponse`.

```swift
import Foundation

func send(order: Data, session: URLSession) async throws -> Int {
    var request = URLRequest(url: URL(string: "https://api.example.com/orders")!)
    request.httpMethod = "POST"
    let (_, response) = try await session.upload(for: request, from: order)
    return (response as? HTTPURLResponse)?.statusCode ?? 0
}
```

In an upload task the body is not placed inside the request but passed in the `from:` parameter. This snippet also stays SOURCE. What happens when a `POST` request that creates a new order is repeated is one of this lesson's six operations.

## Six Operations

The **model** below builds what each operation does to the state; it is not the network or the disk itself. Each of the six operations corresponds to a familiar job in the network layer: writing to a cache, a send with an idempotency key, an order that is retried, incrementing a badge count, writing a preference, appending a line to a journal. With the core's `replay` function each one is applied once, twice, and in two orders.

```swift
// main.swift — six operations, four sensitivities
func cached(_ key: String) -> ([String]) -> [String] {
    { keys in Array(Set(keys + [key])).sorted() }
}

func submit(_ key: String) -> ([String]) -> [String] {
    { received in received.contains(key) ? received : received + [key] }
}

func placeOrder(_ count: Int) -> Int { count + 1 }

func badge(_ count: Int) -> Int { count + 1 }

func theme(_ value: String) -> (String) -> String {
    { _ in value }
}

func log(_ line: String) -> ([String]) -> [String] {
    { lines in lines + [line] }
}

let cacheWrite = replay("cache-write", from: [String](), cached("profile"), then: cached("feed")).report()
let keyedSend = replay("keyed-send", from: [String](), submit("k-41"), then: submit("k-42")).report()
let retry = replay("retry", from: 0, placeOrder, then: placeOrder).report()
let push = replay("badge-increment", from: 3, badge, then: badge).report()
let preference = replay("preference-write", from: "system", theme("dark"), then: theme("light")).report()
let journal = replay("journal-append", from: [String](), log("sign-in"), then: log("sign-out")).report()

countSensitivities([cacheWrite, keyedSend, retry, push, preference, journal])
```

```
cache-write | once=["profile"] twice=["profile"] ab=["feed", "profile"] ba=["feed", "profile"] -> robust
keyed-send | once=["k-41"] twice=["k-41"] ab=["k-41", "k-42"] ba=["k-42", "k-41"] -> order-sensitive
retry | once=1 twice=2 ab=2 ba=2 -> repeat-sensitive
badge-increment | once=4 twice=5 ab=5 ba=5 -> repeat-sensitive
preference-write | once=dark twice=dark ab=light ba=dark -> order-sensitive
journal-append | once=["sign-in"] twice=["sign-in", "sign-in"] ab=["sign-in", "sign-out"] ba=["sign-out", "sign-in"] -> doubly-sensitive
robust: 1
repeat-sensitive: 2
order-sensitive: 2
doubly-sensitive: 1
```

`cache-write` is robust: writing the same key twice does not change the key set, and the order of two keys does not change the result, because the set is printed sorted. `keyed-send` is order-sensitive: the idempotency key makes the second arrival of the same request ignored, so `once` and `twice` are the same; but `k-41` and `k-42` are different requests, and the arrival order determines the order of the list. `retry` is repeat-sensitive: the counter grows with every application. `badge-increment` is also repeat-sensitive, for the same reason. `preference-write` is order-sensitive: the last writer wins. `journal-append` is sensitive to both: the line is added twice and the order of the two lines changes.

**NW1.** This measurement builds each of the six operations from a single starting state. From a different starting state an operation's class can change.

**NW2.** `cache-write` keeps the keys as a sorted set. A cache that preserves insertion order must be measured separately.

The distribution is not a finding. One robust, two repeat-sensitive, two order-sensitive, and one doubly sensitive operation came out because we chose the operations. Six other operations would have produced another distribution.

## What Idempotency Solves

`keyed-send` is the most instructive line of this lesson, because it shows that what solves repetition does not solve order. In HTTP, methods for which sending the same request one after another does not change the result are called **idempotent**; `GET` and `PUT` are such methods, `POST` is not. If the server accepts an **idempotency key** that gives a request an identity, a `POST` that creates a new order can also be idempotent: a second request arriving with the same key is ignored.

In the measurement this came out exactly so. `once` and `twice` are the same: repetition was solved. But when there are two different keys, the order still determines the result: `ab` and `ba` are not the same. **Idempotency solves repetition, not order.** Solving order takes another tool: a version number, a mergeable data structure, or ordering on the server. In later lessons we will measure some of these.

## Where It Shows Up in a Real App

The measured order sensitivity is not an artificial laboratory case. A screen sends a profile request as it opens; when the user pulls down to refresh, a second request goes out. If the two responses return from the network in reverse order, the last one to arrive is written to the screen, and it may be the response to the older request. The result is that a change the user made a moment ago appears to be silently undone on screen. There is no trace of this in the error log: no request failed, and each response is correct on its own.

Retry, on the other hand, is the typical example of repeat sensitivity. When a request that sends an order times out, the client sees that no response arrived; but the server may have received the request and recorded the order. If the client sends the same request again, a second order is created on the server. The `twice` column of the measurement shows exactly this situation: in the `retry` line the number has grown once more when the operation is repeated.

The starting-state issue is more insidious still. The same operation can behave differently in an app that opens with an empty cache and in an app that opens with a cache left full from the previous session. The bounding measurement below shows this: the same pair of operations falls into a different class depending on the starting state. For this reason, whether a request is idempotent cannot be told from the request alone; it is also asked which state the operation runs from.

## The Wrong Setup and Its Fix

The most common wrong setup is to apply retry to every failed request without looking at the kind of request. Reading the profile with `GET` breaks nothing when repeated; but if the same rule is applied to a `POST` request that creates an order, every timeout carries the risk of a second order.

The fix has two steps. The first is to attach an idempotency key to the repeat-sensitive request: the client generates the key on the first attempt and sends the same one on every retry, and the server ignores a second request that arrives with the same key. The `once` and `twice` columns coming out the same in the `keyed-send` line of the measurement is the counterpart of this. The second is that, for order-sensitive responses, the screen accepts only the response to the most recently sent request; a late-arriving response to an older request is ignored. This does not remove the order problem, but it makes the order visible in a single place; the way an order-sensitive operation becomes safe is, most of the time, that the order can no longer change.

For the badge the fix is different. Writing the absolute value the server reports, instead of incrementing the count locally, protects the operation against repetition: even if the same value is written a second time, the result does not change. The absolute value has its own question too: if two values arrive in reverse order, the older one stays. We did not measure this separately in this lesson; we only state its direction.

## Reading the SOURCE Snippets

The two SOURCE snippets in the lesson are not run, because both reach out to the network and their results depend on the server. The snippets are still worth reading. `fetchProfile` is a read: sending the same request twice does not change the state on the server, and its counterpart in the model is the `cache-write` line. `send(order:session:)` is a `POST` request that creates a new record, and it corresponds to the `placeOrder` function in the model. The difference between the model and the real thing is that in a real request the repetition can also happen without the client's knowledge: when the connection drops, the client cannot know whether the request reached the server. This difference does not change the measured class; it only says where the repetition can come from.

The status code check in the snippets matters too. `fetchProfile` accepts only 200; `send(order:session:)` returns the status code as it is and leaves the decision to the caller. On a timeout no response arrives at all, and the client does not know whether the request was processed. Treating this situation as the same error as a request the server explicitly rejected makes the retry send a repeat-sensitive request a second time.

## Common Mistakes

- Creating a new session for every request. The configuration and the connections are not shared.
- Not checking whether the response is an `HTTPURLResponse` and not checking the status code. The call does not throw even when the server returns an error code.
- Taking a large body into memory with a data task. A download task writes to a file.
- Ignoring a repeated user action in the network layer. Pressing the button twice produces two requests; the repetition question is answered separately on the client and on the server.
- Using the idempotency key on the assumption that it also solves the order problem.

## Interpreting the Measured Result

All six operations were jobs that can show up in a network layer, and they spread across four categories. What matters is not the distribution but that an operation's class comes from two questions: what happens if it is repeated, and what happens if its order changes. The answers to these two questions vary from one operation to another and often cannot be inferred from the operation's name. Although `placeOrder` and `badge` have the same body, one is an order and the other is a counter; for the user their consequences are very different, for the rule they are the same.

In the network layer this distinction is useful in three places. In retry: a repeat-sensitive operation produces a second record when retried. In response order: an order-sensitive operation leaves a stale value if two responses arrive reversed. In the cache: a robust operation can be repeated without the risk of error. The next five lessons measure these three places in turn.

## The Bounding Measurement

An operation's class depends not only on the operation but also on the starting state. Below, the same two operations, `x * 2` and `x * 3`, are run from two starting points.

```swift
// main.swift — the same two operations, two starting points
let doubled = replay("from-zero", from: 0, { $0 * 2 }, then: { $0 * 3 }).report()
let fromOne = replay("from-one", from: 1, { $0 * 2 }, then: { $0 * 3 }).report()
countSensitivities([doubled, fromOne])
```

```
from-zero | once=0 twice=0 ab=0 ba=0 -> robust
from-one | once=2 twice=4 ab=6 ba=6 -> repeat-sensitive
robust: 1
repeat-sensitive: 1
order-sensitive: 0
doubly-sensitive: 0
```

Starting from zero, both operations leave zero as zero and the line is robust. Starting from one, the second application changes the value and the line is repeat-sensitive. In both starting states the two orders give the same result for this pair of operations, because the multiplications commute. The same operation fell into another class when the starting state changed.

**NW3.** This measurement compares only the starting points zero and one. Other starting points were not measured, and the class may change at them too.

## Summary

- A network client is built from three parts: the configuration states the behavior, the session works with that behavior, and the task carries out a single transfer.
- A data task takes the response into memory, an upload task sends the body separately, and a download task writes the response to a file.
- Sensitivity is measured with two questions: if it is repeated, and if its order changes, does the result change.
- The six operations fell into four categories: one robust, two repeat-sensitive, two order-sensitive, one doubly sensitive.
- Idempotency solves repetition, not order; the same operation can fall into another class when the starting state changes.
- This lesson's sensitivity: **all four**.

## Next Step

The response that comes back from the network is a byte sequence; what the application needs is a typed record. In the next lesson, JSON Encoding and Decoding will measure whether turning a response into a record can itself be order-sensitive: the order in which a two-version record is written determines the value in the store.
