InnoNetwork is a Swift package for type-safe networking on Apple platforms.
Most applications import only the root InnoNetwork product and use three
concepts:
- an explicit endpoint
structthat owns inputs andAPIResponse @APIDefinitionto derive and validate repetitive protocol witnessesDefaultNetworkClient.request(_:)to execute the typed request
Everything else—including Download, WebSocket, persistent cache, OpenAPI, AWS signing, pinning, and test support—is an optional product selected only when that capability is required.
Release status:
4.0.0is the latest tagged stable release and the actively security-supported line. Themainbranch is an unreleased, source-breaking 5.0 preview; no5.0.0tag exists yet. Unless a section says otherwise, the API examples below describe the currentmainpreview and may not compile against 4.x.
| Product | Use When |
|---|---|
InnoNetwork |
Start here for named typed HTTP endpoints and the async request pipeline. Advanced policy remains opt-in. |
InnoNetworkAuthAWS |
You need the optional AWS SigV4 reference signer. It is a single-shot signer, not an AWS SDK replacement. |
InnoNetworkDownload |
You need foreground/background download lifecycle management with pause, resume, retry, persistence, and event streams. |
InnoNetworkWebSocket |
You need long-lived bidirectional connections with heartbeat, reconnect, close taxonomy, and event delivery. |
InnoNetworkPersistentCache |
You want ResponseCache backed by disk with conservative RFC-aware storage guards and data protection. |
InnoNetworkOpenAPI |
Use OpenAPIRequest when generated or hand-written operations should run through the full DefaultNetworkClient pipeline. Use InnoNetworkClientTransport when an OpenAPI Runtime client needs a thin URLSession-backed transport and the full pipeline is not required. |
InnoNetworkTrust |
You need optional public-key pinning via PublicKeyPinningEvaluator and TrustPolicy.custom(_:). |
InnoNetworkTestSupport |
You need consumer-test helpers such as MockURLSession, StubNetworkClient, or WebSocket recorders. Do not link it into production binaries. |
For application API catalogs, start with an explicit endpoint struct and the
default-enabled @APIDefinition macro. The struct remains the source of truth;
the macro derives repetitive witnesses and fails the build when method, path,
payload, response, or auth declarations are incomplete. Use EndpointBuilder
for one-off or runtime-composed requests that do not deserve a named contract.
Start with only the InnoNetwork product and
DefaultNetworkClient(baseURL:). A named endpoint struct plus
@APIDefinition needs no configuration pack or optional product. Add an
advanced pack only when a concrete retry, auth, cache, transport, or
observability requirement appears; add Download, WebSocket, persistent cache,
OpenAPI, AWS auth, or pinning products only for the capability named in the
table above. If the application has only one or two uncomplicated requests and
no shared policy, direct URLSession is intentionally the smaller choice.
Applications that prefer manual APIDefinition conformances can disable the
macro target with traits: []. This is an opt-out for build topology, not a
second runtime API.
The packages are built around Swift Concurrency, explicit transport policies, and operational visibility that can scale from app prototypes to production clients.
Five things that differentiate this library from the usual URLSession wrapper or Alamofire-style helper:
typed throwsend-to-end —NetworkClient.request(_:)is declared asasync throws(NetworkError), so call-sites get a concrete error type incatchwithout losing classification. Foreign errors are mapped at a single narrow boundary so the typed-throws invariant holds.- Phantom-typed HTTP headers —
HTTPHeader<Value>keys carry their value type at compile time (e.g..contentType,.authorization, custom phantom keys). Typos and value/type mismatches fail at build, not at runtime. - Explicit session authentication — every endpoint declares
SessionAuthenticationas.anonymous,.optional, or.required. Required endpoints fail before transport when no refresh policy can provide a token; the single-flightRefreshTokenPolicyonly refreshes endpoints that opted in. - Single-flight refresh + idempotency-aware retry — concurrent 401s
coalesce into one refresh call (
RefreshTokenCoordinator). Retries follow RFC 9110:GET,HEAD,OPTIONS, andTRACEretry by default; unsafe methods retry only when anIdempotency-Keyheader is present.Retry-Afteris parsed for all three RFC 9110 formats with a maximum-clamp against malicious headers. - RFC 9111-aware cache adapter + first-class test support — opt into
rfc9111Compliant(wrapping:)to get the documented directive subset, or drop inMockURLSession/VCRURLSession/StubNetworkClientfromInnoNetworkTestSupport(a top-level product, not a hidden helper).
See API_STABILITY.md for the Stable / Provisionally Stable contract
around each of these.
⚠️ Apple platforms only by design. InnoNetwork builds on URLSession,OSAllocatedUnfairLock, OSLog, and Network.framework, none of which match Apple-platform behaviour on Linux. Linux/server-side Swift is not a supported target. See docs/PlatformSupport.md for the rationale and for guidance on sharing models with Linux server code (e.g. Vapor).
📚 API Reference (DocC): https://innosquadcorp.github.io/InnoNetwork/ 🇰🇷 한국어 문서: docs/ko/README.md
InnoNetwork ships several layers. Pick the highest one that preserves the
contract your application needs. Most app teams should use macro-assisted,
explicit APIDefinition structs for their API catalog.
Does this endpoint belong in the application's named API catalog?
├─ yes ─► @APIDefinition on an explicit struct
│ (the struct owns inputs, APIResponse, and any custom policy)
└─ no
│
Is it a one-off or runtime-composed HTTP request?
├─ yes ─► EndpointBuilder
└─ no
│
Does it need multipart, streaming, or generated OpenAPI transport?
├─ yes ─► MultipartAPIDefinition / StreamingAPIDefinition / InnoNetworkOpenAPI
└─ no
│
Are you building an SDK or library wrapper that needs raw
transport hooks (no decoding, no interceptors)?
└─ yes ─► LowLevelNetworkClient (@_spi(GeneratedClientSupport))
— minor-version-mutable surface, see API_STABILITY.md
- Linux / server-side Swift. InnoNetwork is Apple-platform-only by
design (URLSession,
OSAllocatedUnfairLock, OSLog, Network.framework). Use AsyncHTTPClient or a server framework on Linux. - One-off scripts where URLSession's three-line GET is enough. The type-safety surface only pays off once you have shared interceptors, retry/coalescing/cache policies, or a fleet of endpoints.
- Push-style protocols outside HTTP and WebSocket. gRPC, MQTT, WebTransport are out of scope; pair InnoNetwork with a dedicated client.
- You need synchronous APIs. The package is built on Swift
Concurrency end-to-end; there is no
DispatchQueue/completion-handler fallback.
For released applications, consume the tagged 4.x line:
dependencies: [
.package(
url: "https://github.com/InnoSquadCorp/InnoNetwork.git",
.upToNextMajor(from: "4.0.0")
)
]To evaluate the breaking 5.0 preview, opt into main explicitly. Prefer a
specific revision in CI so preview updates are deliberate:
dependencies: [
.package(
url: "https://github.com/InnoSquadCorp/InnoNetwork.git",
branch: "main"
)
]Do not treat
mainas a released SemVer dependency. The draft Stable / Provisionally Stable ledger inAPI_STABILITY.mdbecomes the 5.x contract only when5.0.0is tagged.InnoNetwork also intentionally requires Swift 6.2+ and current Apple OS baselines (iOS 16, macOS 14, tvOS 16, watchOS 9, visionOS 1). That keeps the package aligned with strict concurrency and modern URLSession behavior, but apps with older deployment targets should keep a thin compatibility client until they can raise their platform floor.
The following current API uses the unreleased 5.0 preview. For the tagged 4.x API, start with the 4.0 release and migration documents instead.
import Foundation
import InnoNetwork
struct User: Decodable, Sendable {
let id: Int
let name: String
}
struct CreatePost: Encodable, Sendable {
let title: String
let body: String
}
struct Post: Decodable, Sendable {
let id: Int
let title: String
}
@APIDefinition(method: .get, path: "/users/{id}", auth: .anonymous)
struct GetUser {
typealias APIResponse = User
let id: Int
}
@APIDefinition(method: .post, path: "/posts", auth: .anonymous)
struct CreatePostEndpoint {
typealias APIResponse = Post
let body: CreatePost
}
let client = DefaultNetworkClient(
baseURL: URL(string: "https://api.example.com/v1")!
)
let user = try await client.request(GetUser(id: 1))
let created = try await client.request(
CreatePostEndpoint(body: CreatePost(title: "Hello", body: "World"))
)
print(user)GetUser and CreatePostEndpoint remain ordinary, explicit value types. The
macro adds APIDefinition conformance, method, path, and the simple
Parameter / parameters witnesses. APIResponse stays visible, and every
attribute must choose auth: .anonymous, .optional, or .required; the
macro never guesses a security boundary. A stored query is inferred for GET
and HEAD; a stored body is inferred only for POST, PUT, PATCH, and DELETE.
OPTIONS, CONNECT, TRACE, custom, and dynamic methods require a complete
Parameter + parameters payload contract.
Path placeholder values must be non-optional. Direct T?, Optional<T>, and
Swift.Optional<T> spellings fail during macro expansion; aliases that resolve
to an Optional fail at the generated call site with the same targeted guidance
to unwrap the value and define its nil behavior.
Use EndpointBuilder when a request is genuinely local or runtime-composed:
let previewPath = "/users/preview"
let preview = try await client.request(
EndpointBuilder<User>
.get(previewPath)
.authentication(.anonymous)
)DefaultNetworkClient(baseURL:) is exactly the safeDefaults client path.
Move to init(configuration:) only when the server contract requires an
explicit policy. EndpointBuilder<Response>.get/post/... infers the default
JSON response decoder from Response; the existing .decoding(_:) promotion
remains available when builder composition starts from EmptyResponse.
For a custom payload, declare the complete Parameter + parameters pair.
It is the authoritative escape hatch; headers, interceptors, transport,
decoding, and policy overrides likewise stay explicit on the struct.
struct UserPatch: Encodable, Sendable {
let displayName: String
}
@APIDefinition(method: .patch, path: "/me", auth: .anonymous)
struct UpdateUser {
typealias Parameter = UserPatch
typealias APIResponse = User
let patch: UserPatch
var parameters: Parameter? { patch }
var transport: TransportPolicy<User> {
.json(decoder: snakeCaseDecoder)
}
}APIDefinition exposes one transport-shape entry point —
transport: TransportPolicy<APIResponse>. For macro-assisted simple payloads,
GET and HEAD use .query(), while POST, PUT, PATCH, and DELETE use .json().
Other methods keep their payload and any non-default transport explicit on the
endpoint struct.
let patched = try await client.request(
UpdateUser(patch: UserPatch(displayName: "Taylor"))
)The macro comes from import InnoNetwork through the package's default
Macros trait. No separate codegen package or import is required. Consumers
that never use macros can set traits: [] on the InnoNetwork package
dependency; this removes the macro declaration and compiler plug-in products
from their target graph and compilation. SwiftPM may still resolve or fetch
the package-level swift-syntax dependency while evaluating the manifest.
Traits are unified per package across the resolved graph, so every dependency
path must keep Macros disabled; another dependency that enables the default
trait re-enables it for that package instance.
Use these after the first request path is stable:
MultipartAPIDefinitionfor upload payloads that need explicit part boundaries, metadata, and retry idempotency.StreamingAPIDefinitionfor SSE, NDJSON, logs, or other line-delimited long-lived responses.InnoNetworkWebSocketfor bidirectional realtime flows.InnoNetworkOpenAPIfor generated clients or OpenAPI Runtime transport.InnoNetworkPersistentCachewhen an API needs on-disk RFC-aware response caching beyond the in-memory default.
import Foundation
import InnoNetworkDownload
// Construct one DownloadManager per feature (or per policy). Each
// instance binds a unique URLSession identifier and DownloadConfiguration,
// so a media downloader can be WiFi-only and resumable while a documents
// downloader uses a different retry budget.
let manager = try DownloadManager(
configuration: .safeDefaults(sessionIdentifier: "com.example.app.media")
)
let task = await manager.download(
url: URL(string: "https://example.com/file.zip")!,
toDirectory: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
)
for await event in await manager.events(for: task) {
print(event)
}
// The owner closes the lifecycle explicitly. This cancels and removes
// persisted in-flight work, drains admitted callbacks/events, and releases
// the manager's session and persistence scope.
await manager.shutdown()safeDefaults and advanced use the secure foreground session mode. Call
backgroundTransfersEnabled() on the finished configuration only when
transfers must continue outside the app process; see the background-mode
redirect trade-off below and in the
background download guide.
The 4.0.0 line removes the global
DownloadManager.sharedsingleton — every feature now constructs and owns its own manager viaDownloadManager/init(configuration:)with a unique session identifier. The throwing initializer surfacesDownloadManagerError(e.g.,duplicateSessionIdentifier) directly so the failure mode is explicit.
download(url:toDirectory:fileName:) resolves the destination as:
- If
fileName:is provided, it is trimmed and used only when it is a safe single path component. - Otherwise the URL's last path component (
url.lastPathComponent) is used when it is a safe single path component. - Empty names,
.,.., or names containing/,\, or NUL fall back to a generateddownload-<UUID>filename underdirectory. - The library does not rename on collision — if a file already exists at the resolved
path, the download will overwrite it once it completes. Pass an explicit
fileName:(for example, prefixed with the task UUID) when concurrent or repeated downloads to the same directory must coexist.
For absolute control over the destination path, use download(url:to:) instead and
construct the target URL yourself.
import Foundation
import InnoNetworkWebSocket
let manager = WebSocketManager(configuration: .safeDefaults())
let task = await manager.connect(
url: URL(string: "wss://echo.example.com/socket")!
)
for await event in await manager.events(for: task) {
print(event)
}- async/await request execution
- type-safe
APIDefinitionmodeling - JSON, form-url-encoded, and multipart request support
- retry coordination, stable idempotency keys, auth refresh, request coalescing, response cache, and circuit breaker policies
- streaming-by-default inline response buffering and public
RequestExecutionPolicyhooks - W3C
traceparentpropagation and curl command export helpers - explicit
.anonymous,.optional, and.requiredsession authentication throughAPIDefinitionandEndpointBuilder - trust policy support and request lifecycle observability
- foreground and background download orchestration
- pause, resume, retry, and listener retention across retries
- append-log persistence for durable task restoration
AsyncStreamand listener-based event delivery
- heartbeat and pong timeout handling
- reconnect policies with handshake-aware close taxonomy
- listener retention across automatic reconnect transport generations
- explicit
retry(_:)returns a fresh task and pre-registered bounded event stream inWebSocketRetryResult, preventing immediate retry events from racing consumer registration AsyncStreamand listener-based event delivery
- actor-backed
ResponseCacheimplementation for on-disk GET caching - default 50 MB / 1000 entry / 5 MB per-entry caps
- refuses authenticated,
Cache-Control: private, andSet-Cookieresponses by default - applies
.completeUntilFirstUserAuthenticationdata protection to cache files by default on iOS-family platforms - excludes cache-owned indexes, keys, and bodies from backup while leaving the caller-supplied directory root unchanged
dataProtectionClass: .nonerequestsNSFileProtectionNonefor cache-owned paths on iOS-family platforms- versioned index and hashed body files with corrupt-entry eviction
OpenAPIRequestfor running generated or hand-written operations through the fullDefaultNetworkClientpipelineOpenAPIRestOperationbridge for generated operation metadataInnoNetworkClientTransportfor thinswift-openapi-runtimetransport when URLSession-level behavior is enough- generated-client redirects use the default redirect policy plus per-hop URL admission; background URLSession is rejected because Foundation bypasses that callback
- public-key pinning evaluator split from the core product
PublicKeyPinningPolicyhost rules and SPKI matchingTrustPolicy.custom(_:)integration for HTTP and WebSocket trust evaluation
MockURLSessionfor deterministic request capture in consumer testsStubNetworkClientfor testing app layers without touching transportWebSocketEventRecorderfor websocket integration assertions- intended for test targets, not production binaries
- default-enabled
@APIDefinition(method:path:auth:)fromimport InnoNetwork - explicit endpoint structs remain the source of truth
APIResponseand authentication intent stay mandatory and visible- simple GET/HEAD
queryor POST/PUT/PATCH/DELETEbodyproperties derive payload witnesses - OPTIONS, CONNECT, TRACE, custom, and dynamic methods require a complete
Parameter+parameterspayload contract - complete
Parameter+parametersdeclarations remain authoritative - compile-time diagnostics reject incomplete or ambiguous definitions
traits: []excludes macro APIs and compiler plug-in compilation for core-only consumers
- iOS 16.0+
- macOS 14.0+
- tvOS 16.0+
- watchOS 9.0+
- visionOS 1.0+
- Swift 6.2+
The package intentionally targets current Apple platform releases. That lets the codebase rely on modern Swift Concurrency semantics, stricter Sendable checking, and the latest URLSession and platform APIs without compatibility shims.
Protocol Buffers support moved to the separate InnoNetworkProtobuf package. Consumers that need protobuf request and response modeling must add InnoNetworkProtobuf alongside InnoNetwork in the same package manifest.
dependencies: [
.package(url: "https://github.com/InnoSquadCorp/InnoNetwork.git", branch: "main"),
.package(url: "https://github.com/InnoSquadCorp/InnoNetworkProtobuf.git", branch: "main")
]InnoNetworkProtobuf is being prepared for its first tagged release; until
then, follow its main branch. This pair is a preview configuration, not a
tagged production dependency.
Use safeDefaults as the secure baseline for prototypes, tests, and production
clients that do not yet have measured reasons for additional policy. Use
advanced only when the integration explicitly needs retry, cache, auth,
observability, or transport tuning. There is intentionally no universal
"production" preset: retry, circuit-breaker, and idempotency semantics depend
on the server contract and should not be enabled by a generic label.
In the 5.0 preview, safeDefaults and the advanced preset cap collected
responses, including file-upload responses, at 5 MiB by default.
Set an explicit .streaming(maxBytes: nil) or .buffered(maxBytes: nil) only
when an unbounded response is a deliberate, reviewed choice.
For inline requests, .buffered(maxBytes:) validates the size only after
URLSession.data(for:) has buffered the response; it bounds what proceeds to
cache storage and decoding, not peak transport buffering. Bounded streaming
and bounded file-upload responses inspect the body while it arrives and cancel
the underlying task as soon as a known or observed limit is exceeded. A
bounded file upload therefore uses a streamed data task with an explicit
Content-Length; an explicitly unbounded file upload preserves the native
file-backed upload-task path.
import Foundation
import InnoNetwork
let network = NetworkConfiguration.safeDefaults(
baseURL: URL(string: "https://api.example.com")!
)
let tunedNetwork = NetworkConfiguration.advanced(
baseURL: URL(string: "https://api.example.com")!,
resilience: ResiliencePack(
retry: ExponentialBackoffRetryPolicy(),
coalescing: .getOnly
),
cache: CachePack(
responseCachePolicy: .cacheFirst(maxAge: .seconds(60)),
responseCache: InMemoryResponseCache(),
sensitiveHeaderNames: ["X-Tenant-Token"]
),
transport: TransportPack(
timeout: 30,
redirectPolicy: DefaultRedirectPolicy(),
trustPolicy: .systemDefault
)
)Auth refresh, coalescing, caching, and circuit breaking are opt-in. The
request execution pipeline stays internal. Configuration values are opaque
immutable commands: use presets and named pack inputs, and keep an
application-owned settings model only when product code must inspect the
chosen values. Optional Download and WebSocket products follow the same
safeDefaults-first rule.
Core request clients and downloads default to HTTPS-only construction and safe redirect handling:
DefaultRedirectPolicyrejects HTTPS-to-HTTP downgrade redirects.- Any cross-origin redirect that retains an unsafe method such as POST, PUT, PATCH, or DELETE is rejected, including nonstandard 301/302 proposals as well as 307/308 replay.
- Other cross-origin redirects strip every header prepared on the original
request, plus common authorization, cookie, API-key, CSRF/session-token, and
temporary AWS credential headers. Core URLSession transports also clear
every value from
URLSessionConfiguration.httpAdditionalHeaderson a cross-origin hop so Foundation cannot re-inject a removed session default; same-origin redirects retain those values. Register proprietary credential carriers withadditionalSensitiveHeaderswhen using the policy outside that transport integration; built-in protection remains enabled. - Plain
http://base URLs fail before transport unless the client opts in withallowsInsecureHTTP = true. - Foreground downloads — the configuration default — re-admit every redirect
target through the same URL guard. A rejected
downgrade, unsafe cross-origin replay, or traversal target fails as
DownloadError.invalidURLwithout consuming retry budget. backgroundTransfersEnabled()is the one explicit opt-in that trades that per-hop preflight for process-independent continuation. Foundation follows background redirects automatically without consulting the redirect delegate. Initial and final URLs are still validated where Foundation exposes them, but that validation cannot prevent an intermediate background redirect from being contacted.- Download progress callbacks are coalesced before entering the actor, while completion callbacks remain lossless and FIFO. Final completed, failed, and cancelled events seal their task partition so late progress cannot displace the terminal outcome under a bounded overflow policy.
- App-facing download callbacks use a per-task ordered delivery lane. A slow
progress or terminal callback does not hold the URLSession delegate FIFO or
delay the system background-session completion handler. The pre-transport
waitinganddownloadingstate callbacks remain admission hooks, and externalshutdown()still drains every accepted callback before returning. - Base URLs with embedded
user:password@hostcredentials or fragments are rejected; put credentials in an interceptor orRefreshTokenPolicyinstead.
let redirectPolicy = DefaultRedirectPolicy(
additionalSensitiveHeaders: ["X-Tenant-Secret"]
)Two explicit compatibility switches exist for controlled legacy or LAN environments. Enabling them weakens the default boundary, so scope the policy to a dedicated client; sensitive headers are still stripped across origins:
let controlledLegacyRedirects = DefaultRedirectPolicy(
allowsHTTPSDowngrade: true,
allowsCrossOriginUnsafeMethodRedirects: true
)Keep the HTTP opt-in scoped to local development or a controlled LAN-only client:
let local = NetworkConfiguration.advanced(
baseURL: URL(string: "http://localhost:8080")!,
transport: TransportPack(allowsInsecureHTTP: true)
)let refreshPolicy = RefreshTokenPolicy(
currentToken: { try await tokenStore.currentAccessToken() },
refreshToken: { try await authService.refreshAccessToken() }
)
let client = DefaultNetworkClient(
configuration: .advanced(
baseURL: URL(string: "https://api.example.com")!,
resilience: ResiliencePack(
circuitBreaker: CircuitBreakerPolicy(failureThreshold: 3)
),
auth: AuthPack(refreshToken: refreshPolicy)
)
)request(_:tag:) and upload(_:tag:) register the dispatched task under a
CancellationTag so a screen, feature, or user session can drop just its own
requests when it goes away. cancelAll() continues to drain every in-flight
request when no granularity is required.
let feed: CancellationTag = "feed"
async let posts = client.request(GetPosts(), tag: feed)
async let banner = client.request(GetBanner(), tag: feed)
// User leaves the feed screen — only feed-tagged requests stop:
await client.cancelAll(matching: feed)Untagged requests, and requests registered with a different tag, are left
alone by cancelAll(matching:).
The opt-in ResponseCachePolicy honours the response Vary header
automatically (RFC 9111 §4.1):
Vary: *responses are not stored — the cache cannot prove a future request would match.- A concrete
Varyheader (for exampleVary: Accept-Language) captures the named request headers when the response is stored. The next lookup matches only when those same header values are present, so two clients with differentAccept-Languagevalues do not see each other's payloads. - Responses without a
Varyheader use the existing per-identity key (Authorization, etc.) when the response is eligible for storage. - GET responses with whole-response cacheable status codes (
200,203,204,300,301,308,404,405,410,414, and501) can be stored;206 Partial Contentis excluded. Cache-Control: no-storeandCache-Control: privateinvalidate the current cache key and skip writes, including quoted directives such asprivate="Set-Cookie, Authorization".Cache-Control: no-cachestores the response but forces revalidation before every reuse.- Responses to requests carrying
Authorizationare stored only when the origin explicitly permits it withCache-Control: public,must-revalidate, ors-maxage. - Successful unsafe methods (
POST,PUT,PATCH,DELETE, and unknown methods) invalidate every cached variant for the normalized target URI per RFC 9111 §4.4..disabledand.networkOnlystill leave cache metadata untouched.
InnoNetworkPersistentCache is not a full RFC 9111 cache by
default — storage directives are enforced by the executor, while
the freshness window is normally driven by ResponseCachePolicy
(cacheFirst(maxAge:) etc.). Clients that need directive-aware
behavior (no-store suppression, must-revalidate stale denial,
server max-age clamping, Expires fallback, and Last-Modified heuristic freshness)
should wrap their policy with
ResponseCachePolicy.rfc9111Compliant(wrapping:):
let cache = CachePack(
responseCachePolicy: .rfc9111Compliant(
wrapping: .cacheFirst(maxAge: .seconds(300))
),
responseCache: InMemoryResponseCache()
)See docs/rfcs/RFC9111-Compliance.md for the exact directive coverage
and the trade-offs.
The default Macros trait exposes @APIDefinition directly from
import InnoNetwork:
import InnoNetwork
@APIDefinition(method: .get, path: "/users/{id}", auth: .anonymous)
struct GetUser {
typealias APIResponse = User
let id: Int
}The attribute derives conformance, method, percent-encoded path,
sessionAuthentication, and simple payload witnesses. It does not hide the
response model or endpoint policy: APIResponse, stored inputs, headers,
interceptors, transport, and decoding choices remain visible on the struct. A
complete Parameter + parameters pair overrides body/query inference for
advanced endpoints.
Invalid definitions fail at compile time with a diagnostic and, where safe, a
Fix-It. If an otherwise ordinary struct omits @APIDefinition, Swift cannot
know its intent at the declaration. Passing it to either request overload is
the first provable endpoint boundary and produces a targeted error that asks
for the macro or a manual APIDefinition conformance. The 4.x #endpoint
expression macro is removed; use EndpointBuilder when a request does not need
an explicit endpoint type.
See Using Macros for payload rules, diagnostics, and core-only trait opt-out.
InnoNetwork favors explicit transport errors over opaque failures.
do {
let user = try await client.request(GetUser(id: 42))
print(user)
} catch {
switch error {
case .configuration(reason: .invalidBaseURL(let url)):
print("Invalid base URL: \(url)")
case .configuration(reason: .invalidRequest(let message)):
print("Invalid request configuration: \(message)")
case .configuration(reason: .offline(let message)):
print("Offline: \(message)")
case .statusCode(let response):
print("Unexpected status code: \(response.statusCode)")
case .decoding(let stage, let underlying, _):
print("Decoding failed (\(stage)): \(underlying)")
case .trustEvaluationFailed(let reason):
print("Trust evaluation failed: \(reason)")
case .cancelled:
print("Request cancelled")
default:
print(error)
}
}.configuration(reason: .invalidRequest(...)) usually means request shape and policy do not match. Common examples are:
- sending a top-level scalar or array query without
queryRootKey - mismatching
contentTypeand request payload semantics - putting query or fragment components directly in an endpoint
path - using a malformed multipart payload
Request construction follows a fixed contract: endpoint paths are appended after
the baseURL path even when they start with /; endpoint paths must not include
? or #; query values must flow through parameters and queryEncoder; and
Content-Type is attached only for body, multipart, or file-upload payloads.
Header precedence is library defaults, endpoint headers, automatic body
Content-Type, request interceptors, then RefreshTokenPolicy authorization.
Choose HTTPHeaders.add only when a repeated field value is intentional, such
as accept negotiation or other comma-combinable metadata. Prefer
HTTPHeaders.update, subscript assignment, or URLRequest.setValue for
single-value request fields such as Authorization, Content-Type, Cookie,
and Host; URLRequest.headers and URLSessionConfiguration.headers apply
single-value names with last-write-wins semantics while preserving repeatable
header values.
For operational tuning, see Examples and API Stability. Teams migrating an existing client can start with Migration Guides, then use the focused Alamofire cookbook or Moya cookbook for 30-minute before/after examples.
Public releases follow semantic versioning. 4.0.0 is the latest tagged
compatibility baseline. The current main branch is preparing the breaking
5.0 contract, but that contract is still a preview until 5.0.0 is tagged.
- Stable public API: API_STABILITY.md
- Release rules and compatibility policy: docs/RELEASE_POLICY.md
- Migration expectations: docs/MIGRATION_POLICY.md
safeDefaults is the canonical secure entry point. The duplicate legacy
configuration aliases are absent from the 5.0 preview; examples and new
integrations use the named factory so the selected policy is visible.
NetworkClient.request and UploadNetworkClient.upload are the recommended
capability boundaries (DefaultNetworkClient supports both). Use
RequestExecutionPolicy to observe or wrap a transport attempt and to adapt
its response. Execution policies cannot replace the executor-owned request;
use RequestInterceptor for URL, header, or body mutation. SPI lower-level
execution hooks remain outside the stable public contract.
For long-lived line-delimited transports (Server-Sent Events, NDJSON, log
streams), use DefaultNetworkClient.stream(_:) together with a
StreamingAPIDefinition. The default stream is lossless and reads at most one
decoded output ahead of its consumer; the explicit buffering-policy overload
offers unbounded or lossy delivery when that trade-off is intentional. To
cancel every in-flight request and stream
(for example, on logout or backgrounding), call
DefaultNetworkClient.cancelAll(). See the draft
5.0 release notes for preview details and the draft
5.0 migration guide for source changes being
prepared. Neither document announces a tagged release.
The repository includes a dedicated benchmark runner for quick local comparisons.
swift run -c release InnoNetworkBenchmarks --quick
swift run -c release InnoNetworkBenchmarks --json-path /tmp/innonetwork-bench.jsonBenchmark governance, baseline policy, and CI posture are documented in Benchmarks/README.md.
Current guarded quick-baseline highlights from
Benchmarks/Baselines/default.json
(generatedAt: 2026-07-14T17:50:26Z):
| Area | Guarded benchmark | Iterations | Baseline ops/sec |
|---|---|---|---|
| Request pipeline | client/request-pipeline |
2,000 | 8,058 |
| Request coalescing | client/request-coalescing-shared-get |
2,000 | 8,844 |
| Cache lookup | cache/response-cache-lookup |
200,000 | 3,051,671 |
| Cache revalidation | cache/response-cache-revalidation |
200,000 | 4,559,491 |
| Event fan-out | events/task-event-fanout-single |
2,000 | 48,157 |
| Download restore | persistence/download-persistence-restore |
50 | 380 |
| WebSocket close classification | websocket/websocket-close-disposition-classify |
500,000 | 75,500,668 |
Operational items to verify before shipping a client built on InnoNetwork.
- TLS pinning rotation. When using
TrustPolicy.custom(PublicKeyPinningEvaluator(...))(fromimport InnoNetworkTrust), ship at least two pins (current + next) and document the rotation cadence so the app keeps validating after certificate replacement. Consider feature-gated rollback to.systemDefaultfor emergency recovery. - Redirect credential leakage. Keep the default
DefaultRedirectPolicyunless you have a stricter allowlist. Any custom policy must preserve the cross-origin stripping ofAuthorization,Cookie, andProxy-Authorization. - Pinning host matching. Keep the default
.unionAllMatchesif parent-domain pins should act as backup pins for subdomains. Use.mostSpecificHostwhenexample.comandapi.example.compins must be operated as separate trust scopes. - App Transport Security (ATS). The default
safeDefaultsconfiguration assumes ATS is enabled. AvoidNSAllowsArbitraryLoadsin productionInfo.plist. If a non-HTTPS host is unavoidable, scope anNSExceptionDomainsentry to that host only and setallowsInsecureHTTP = trueonly on the matching client configuration. - Custom trust evaluation. A
TrustEvaluatingimplementation runs before request bodies are ever decoded, so a rejected challenge becomesNetworkError.trustEvaluationFailed. Surface the failure to a user-facing recovery path; do not auto-retry on trust failure.
- Foreground is the secure default.
DownloadConfiguration.safeDefaultsandadvancedpermit per-hop redirect admission. CallbackgroundTransfersEnabled()only when the product requires transfers to continue outside the app process and accepts Foundation-managed redirects. - Background download Info.plist. A background
URLSessiondownload does not itself requireUIBackgroundModes. Declare a mode only for a separate wake-up mechanism owned by the app, such asremote-notification. - Session identifier uniqueness. Each
sessionIdentifierpassed to a download configuration factory must be unique among live managers in the app process. The library rejects reuse withDownloadManagerError.duplicateSessionIdentifier; for background sessions, the identifier is also the Foundation background-session identifier. App Groups do not make concurrent app/extension ownership safe: coordinate so exactly one process owns a given identifier at a time. - Destination ownership. Assign one active logical download to each final file path. Different sessions or processes are not serialized merely because they share an App Group container.
- Background completion handler. Wire the system-provided completion handler (delivered to
application(_:handleEventsForBackgroundURLSession:completionHandler:)) intoDownloadManagerso the OS releases the app suspension promptly. The handler is paired only with aurlSessionDidFinishEventsthat occurs after that batch is registered; an earlier unscoped finish is never carried forward.
- Redaction defaults.
NetworkEventnever carries headers or bodies. Before any observer receives an event, URL user-info and fragments are removed, query values are redacted, and JWT-like path values are masked; failures use stable payload-free categories. The secureNetworkLoggeradditionally redacts every header value, cookie, body, query value, and free-form error description.NetworkLoggingOptions.verboseis an explicit local-debugging opt-in and must not be used in CI, shared logs, or production. - cURL export.
URLRequest.curlCommand()omits request bodies and redacts every header and query value by default.includesHeaderValues,includesQueryValues, andincludesBodyare independent explicit opt-ins for a controlled local diagnostic path. URL user-info and fragments are never exported. - Failure payload capture.
NetworkError.decoding(stage:, underlying:, response:)carries aResponse; by default thatresponse.datais redacted to empty data unless you opt in viaCachePack(captureFailurePayload: true)in an advanced configuration. Keep that flag off in release configurations to avoid storing PII inside crash logs or analytics. - Event observer attachment. Attach observers (
NetworkEventObserving) at app start and detach on logout / account switch. Observers receive every request event, including ones triggered after a user-initiated cancellation.
- Cancel-on-logout. Call
DefaultNetworkClient.cancelAll()when the user logs out, switches accounts, or backgrounds. Streaming requests (SSE/NDJSON) only stop when their parent task is cancelled. - Retry budget.
ExponentialBackoffRetryPolicy.maxTotalRetriesis the absolute cap that network-monitor recovery does not reset. Budget per user session, not per request. - Retry idempotency. The built-in retry policy retries
GET/HEADby default.POST, upload, and multipart requests retry only when they carry anIdempotency-Key, unless the client opts into.methodAgnostic. - Auth refresh. Prefer
RefreshTokenPolicyover response interceptors for401refresh + replay. The policy single-flights concurrent refreshes and replays each fully adapted request at most once. - Cache and circuit breaker. Enable
ResponseCachePolicyandCircuitBreakerPolicyper client only after deciding the cache freshness and host-failure budget for that API. Cache keys include anAuthorizationfingerprint,Cookie/credential-like headers are fingerprinted when present, andAccept-Languageis included by default; the responseVaryheader further refines lookups,Vary: *responses are skipped, andCache-Control: no-store/private/no-cacheare honoured. - WebSocket reconnect cap.
maxReconnectAttemptslimits successive automatic attempts. After exhaustion, surface the failure to the UI rather than reconnect on every app foreground.
- Background fetch friendly. Streaming or websocket products expect explicit
disconnect()calls before app suspension. ImplementapplicationDidEnterBackgroundcleanup; the OS will not gracefully close sockets on your behalf. - Token refresh. Let
RefreshTokenPolicyapply the current access token and own401refresh + replay. Keep tenant headers, request IDs, and other unsigned metadata inRequestInterceptors; useRequestSignerfor body-dependent signatures that must be recomputed after refresh.
| Area | Smoke check |
|---|---|
| Trust | Hit a host pinned to a wrong certificate and verify NetworkError.trustEvaluationFailed. |
| Retry | Stub a 503 Retry-After: 30 response and confirm the policy honours the header. |
| Background download | Kill the app mid-download, relaunch, and verify DownloadRestoreCoordinator resumes. |
| WebSocket reconnect | Drop the network for >10s, restore, and verify only the configured number of attempts ran. |
| Cancel-all | Trigger cancelAll() while a stream and an upload are in flight; both must terminate with .cancelled. |
- DocC API Reference: https://innosquadcorp.github.io/InnoNetwork/
- Examples: Examples/README.md
- API Stability: API_STABILITY.md
- Client Architecture: docs/ClientArchitecture.md
- Policy Interactions: docs/PolicyInteractions.md
- Operational Guides: docs/OperationalGuides.md
- Platform Support: docs/PlatformSupport.md
- Release Policy: docs/RELEASE_POLICY.md
- Migration Policy: docs/MIGRATION_POLICY.md
- Migration Guides: docs/MigrationGuides.md
- Draft 5.0 Migration Guide: docs/Migration-5.0.0.md
- Alamofire Migration Cookbook: docs/MigrationFromAlamofire.md
- Moya Migration Cookbook: docs/MigrationFromMoya.md
- DocC Deployment: docs/DocC_Deployment.md
- Query Encoding Reference: docs/QueryEncoding.md
- WebSocket Lifecycle: docs/WebSocketLifecycle.md
- Task Ownership: docs/TaskOwnership.md
- Draft 5.0 Release Notes: docs/releases/5.0.0.md
- Roadmap: docs/ROADMAP.md
- 한국어 문서: docs/ko/README.md
Verified production users are listed only after maintainers have explicit permission to name the company or team publicly.
If you use InnoNetwork in production, please open a pull request with the company or team name, the modules in use (Core / Download / WebSocket / PersistentCache), and one line about what helped your adoption decision.
InnoNetwork follows a lightweight maintainer model.
- Support policy: SUPPORT.md
- Contributing guide: CONTRIBUTING.md
- Security reporting: SECURITY.md
- Changelog: CHANGELOG.md
- Code of Conduct: CODE_OF_CONDUCT.md
MIT. See LICENSE.