Async HTTP client for Apple platforms. iOS 18+ / Swift 6 (swift-tools 6.2, language mode [.v6]).
The iOS-shared and global conventions in the parent AGENTS.md files still apply; this layer adds only
what's specific to this repo. Release history and the v8 (major) notes live in CHANGELOG.md.
make test— spins up go-httpbin in Docker, runs the full suite, tears it down. Requires Docker.make httpbin/make httpbin-stop— run go-httpbin on:8080to iterate with plainswift test.swift build— library only; CI also fails on any first-partywarning:.- Integration tests need go-httpbin (
TestConfig.httpbinBaseURL, defaulthttp://127.0.0.1:8080). Without it they fail fast (connection refused) — intended, not a flake. The offline /fake*suites need no server. - Formatting:
.swift-format(120 cols, 4-space). Run./scripts/setup.shonce per clone to enable the.githooks/pre-commithook (formats staged Swift files); CI's format check is the backstop. - CI:
macos-15/ Xcode 26.3 / Swift 6.2.x (stricter region-isolation than local 6.3 — CI is the gate). Jobs: swift-format check, build & test (go-httpbin), a warnings gate, and a dead-doc-link check (scripts/check-doc-links.py).
Networking.swift— thepublic actor: config (auth/headers/interceptors/log/cacheTTLvia async setters),events(), URL composition, public cache entry points (clearCache/reset/destinationURL).Networking+HTTPRequests.swift— the public surface: verb overloads (get/post/…),downloadImage/downloadData, and thefake*helpers.Networking+New.swift— request execution: theRequestBodyenum,createRequest, the interceptor fold (perform), response→Resultmapping, the completion funnel (complete/logCompletion),mapThrownError.Networking+Private.swift— download handlers + the cache shims that delegate toCacheStore.Networking+FormEncoding.swift— flat-Encodable→[String: String]for forms/queries.CacheStore.swift/CacheExpiry.swift— the two-tier disk cache (layout, sharding, sweep, TTL).HTTPInterceptor.swift— the middleware seam +AuthRefreshInterceptor/RetryInterceptor/ResponseValidatorInterceptor.NetworkingError.swift— the categorized error model +ResponseMetadata.NetworkingEvent.swift— theevents()types (NetworkingEvent/RequestContext/Outcome/TransactionMetrics).JSONResponse·DownloadResponse·FakeRequest·FormDataPart·Image·Helpers— supporting types.
Cross-file contracts an agent must hold; the why lives in each file's comments.
- Actor isolation.
Networkingis an actor — isolated members needawait, config is async setters (no external property mutation), no subclassing. - Typed bodies, no
Any. The method picks the encoding (body:JSON /form:url-encoded /parts:+fields:multipart /data:contentType:raw;query:for get/delete), via theRequestBodyenum.T∈ anyDecodable·Data·Void·JSONResponse. - Errors say where it failed, never a stringified catch-all; the core never parses the error body —
ResponseMetadata.bodyis the full bytes, the caller decodes its own envelope. - Interceptors are an onion below the decode layer, over a raw
HTTPExchange; registered outermost-first,nextreplays. Downloads route through too; a GET cache hit flows back out through the chain (the cache is the innermost layer). - Cache reads are pure.
.memory/.nonenever touch disk, so a read can't destroy a durable.memoryAndFilecopy — purging belongs to the write path andclearCache. Sliding TTL keyed on file mtime; sharded layout; one-shard-per-launch background sweep. TheNSCachewarm tier can be evicted by iOS at any time — disk is the durable fallback. - Observability is one hook:
events()(multi-consumerAsyncStream). Built-in logging is separate, synchronous, gated bylogLevel; redaction is log-path only, soevents()carries the real headers. - Region-isolation trap (Swift 6.2): before building the
@Sendableinterceptor chain, read actor state (session/collector) into locals so the closure captures none.
- Always run
make test(Docker) to green and keep the build warning-free before claiming done. - Always update
README.mdandCHANGELOG.mdin the same PR as any public-API change — the README snippets are copy-paste docs and must compile under the actor (await). - Ask first before adding a third-party dependency — the core is intentionally dependency-free.
- Never put cache purging back on the read path:
.memory/.nonereads must stay pure, or an evicted warm tier turns a recoverable miss into permanent loss.