Complete API documentation for EKNetwork library.
- NetworkManager
- NetworkRequest
- HTTPMethod
- RequestBody
- MultipartFormData
- RetryPolicy
- NetworkProgress
- TokenRefreshProvider
- Streaming (NDJSON / SSE)
- Error Types
- Response Types
- UserAgentConfiguration
The main class for managing network requests.
public init(
baseURL: @escaping (() -> URL),
session: URLSessionProtocol = URLSession.shared,
streamingSession: URLSessionStreamingProtocol? = nil,
loggerSubsystem: String = "com.yourapp.networking",
userAgentConfiguration: UserAgentConfiguration? = nil,
responseDecoderProvider: (() -> JSONDecoder)? = nil
)Parameters:
baseURL: Closure that returns the base URL for each request. Use{ myURL }for a fixed URL or a closure that reads from config/environment for dynamic base URL (avoids race conditions when switching environments).session: TheURLSessionProtocolto use for making requests (defaults toURLSession.shared)streamingSession: Optional session used bystream(_:accessToken:)for byte-stream responses (NDJSON / SSE / chunked transfer). Whennil(default) the manager reusessessionif it conforms toURLSessionStreamingProtocol(defaultURLSessiondoes), otherwise falls back toURLSession.shared. Added in 1.6.0; existing call sites stay source-compatible.loggerSubsystem: The subsystem identifier for theLoggerinstanceuserAgentConfiguration: Optional User-Agent configurationresponseDecoderProvider: Optional global JSON decoder provider for responses (overrides per-request decoding when enabled)
Example:
// Fixed base URL
let manager = NetworkManager(baseURL: { URL(string: "https://api.example.com")! })
// Dynamic base URL (e.g. from settings)
let manager = NetworkManager(baseURL: { AppSettings.shared.apiBaseURL })
// Convenience: fixed base URL (wraps URL in a closure)
let manager = NetworkManager(baseURL: URL(string: "https://api.example.com")!)Closure that provides the base URL; call baseURL() to get the current base URL. Each request invokes this closure, so the URL can change between requests without race conditions.
Optional token refresher to handle authentication token renewal. When set, automatically refreshes tokens on 401 responses.
User-Agent configuration. If set, automatically adds User-Agent header to all requests.
Optional global decoder provider for JSON responses. If set, it can override per-request decoding.
Sends a network request and decodes the response.
Parameters:
request: The network request to sendaccessToken: Optional closure that returns the access token for authentication
Returns: Decoded response of type T.Response
Throws: Errors encountered during the request or decoding
Example:
let response = try await manager.send(
SignInRequest(email: "user@example.com", password: "password"),
accessToken: { TokenStore.shared.accessToken }
)Protocol representing a network request. Conforming types define the request path, method, headers, parameters, and response type.
The expected response type, must conform to Decodable.
The path component appended to the base URL.
HTTP method for the request.
Optional HTTP headers to include in the request. Defaults to nil.
Optional query parameters appended to the URL. Defaults to nil.
Content-Type header for the request. Defaults to "application/json".
Optional body sent with the request, supporting multiple encodings. Defaults to nil.
Optional multipart form data for upload requests. Defaults to nil.
Optional progress observer for upload/download progress. Defaults to nil.
Retry policy to apply for this request. Defaults to RetryPolicy().
Optional custom error decoder to extract server-side error responses. Defaults to nil.
Should the request allow retries and token refresh on 401 Unauthorized? Defaults to true.
Optional handler used when the server returns an empty body. Defaults to nil.
When the endpoint returns a successful status with zero-length payload, NetworkRequest invokes this handler instead of trying to decode JSON. If you leave it nil, decodeResponse throws NetworkError.emptyResponse.
Choosing the right approach for empty responses:
-
Use
EmptyResponse(recommended for simple success cases):struct DeleteRequest: NetworkRequest { typealias Response = EmptyResponse // emptyResponseHandler is automatically provided }
Best for endpoints that return 204 No Content or empty bodies where you only need to confirm success. The default implementation ignores any payload and returns
EmptyResponse(). -
Use
StatusCodeResponse(when you need HTTP metadata):struct UpdateRequest: NetworkRequest { typealias Response = StatusCodeResponse // emptyResponseHandler automatically extracts status code and headers }
Best when you need to inspect the HTTP status code or headers from the response. The default implementation copies status code and headers from
HTTPURLResponse. -
Provide custom
emptyResponseHandler(for advanced cases):struct CustomRequest: NetworkRequest { typealias Response = MyCustomResponse var emptyResponseHandler: ((HTTPURLResponse) throws -> MyCustomResponse)? { { response in MyCustomResponse( status: response.statusCode, customHeader: response.value(forHTTPHeaderField: "X-Custom") ) } } }
Only needed when you must synthesize a custom response type from headers, status code, or other metadata that accompanies the empty payload.
Provides a decoder instance for JSON responses. Defaults to JSONDecoder().
Provides an encoder instance for JSON request bodies. Defaults to JSONEncoder().
Whether NetworkManager is allowed to override decoding for this request when a global decoder is configured. Defaults to true.
Decodes the raw response into the associated response type.
Parameters:
data: The response dataresponse: The URL response
Returns: Decoded response of type Response
Throws: Decoding errors
Default Implementation: Handles JSON decoding and empty-response fallbacks.
Enum representing HTTP methods supported by the network layer.
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
case head = "HEAD"
case options = "OPTIONS"
case trace = "TRACE"
case connect = "CONNECT"
}Represents a request body for a network request, supporting various types.
Creates a request body from an encodable object (typically for JSON).
Parameters:
encodable: The encodable object to encodecontentType: The content type (defaults to"application/json")
Creates a request body from raw data.
Parameters:
data: The raw datacontentType: The content type
Creates a request body from an input stream (for large uploads).
Parameters:
stream: The input streamcontentType: The content type
Creates a form URL encoded request body.
Parameters:
parameters: Key-value pairs for form URL encoded data
Content Type: Automatically set to "application/x-www-form-urlencoded"
The RequestBody supports the following content types:
.encodable(Encodable)- JSON-encodable object.raw(Data)- Raw binary or pre-encoded data.stream(InputStream)- Stream for large data uploads.formURLEncoded([String: String])- Key-value pairs for form URL encoded data
Represents multipart form data for file uploads.
Unique boundary string used to separate parts. Automatically generated as UUID.
Array of parts included in the multipart form.
Adds a new part to the multipart form data.
Parameters:
name: Form field namedata: Data contentmimeType: MIME type stringfilename: Optional filename
Encodes the multipart form data into a Data object suitable for HTTP body. Uses safe UTF-8 encoding and escapes quotes/backslashes in name and filename per RFC 2183.
Returns: Encoded Data representing the multipart form, or nil if any header string fails UTF-8 encoding. When used by NetworkManager, nil results in NetworkError.invalidMultipartEncoding.
public struct Part {
public let name: String
public let filename: String?
public let data: Data
public let mimeType: String
}Defines the retry behavior for network requests.
Maximum number of retry attempts.
Delay in seconds before retrying a request.
Closure to determine if a request should be retried based on the encountered error.
public init(
maxRetryCount: Int = 0,
delay: TimeInterval = 1.0,
shouldRetry: @escaping (Error) -> Bool = { /* default implementation */ }
)Default Behavior:
- Does not retry on
NetworkError.unauthorized - Does not retry on
URLError.userAuthenticationRequired - Does not retry on errors conforming to
NonRetriableError(e.g. business-level or user-presentable errors) - Retries on other errors
Observable object to track progress of network uploads or downloads.
Fraction of task completed, ranging from 0.0 to 1.0.
@MainActor
class UploadViewModel: ObservableObject {
@Published var uploadProgress: Double = 0.0
func uploadFile(_ data: Data) async throws {
let progress = NetworkProgress()
progress.$fractionCompleted
.assign(to: &$uploadProgress)
// Use progress in request
struct UploadRequest: NetworkRequest {
var progress: NetworkProgress? { progress }
// ...
}
}
}Protocol for providing token refresh functionality.
Refreshes authentication token if needed. This method is called automatically when a 401 Unauthorized response is received.
Example:
class TokenManager: TokenRefreshProvider {
func refreshTokenIfNeeded() async throws {
let refreshRequest = RefreshTokenRequest(
refreshToken: TokenStore.shared.refreshToken
)
let response = try await networkManager.send(refreshRequest, accessToken: nil)
TokenStore.shared.accessToken = response.accessToken
}
}Available since 1.6.0.
send(_:accessToken:) is designed for endpoints that return a complete Decodable body. For endpoints that emit data incrementally — newline-delimited JSON, Server-Sent Events, chunked log/inference streams — use stream(_:accessToken:). The streaming entry point reuses the exact same request-construction pipeline (headers, Authorization, User-Agent, body, base URL), so app-level code never has to build a URLRequest by hand and risk dropping required headers like X-Device-ID or custom auth.
public protocol NetworkStreaming: AnyObject {
func stream<T: NetworkRequest>(
_ request: T,
accessToken: (() -> String?)?
) async throws -> StreamingResponse
}NetworkManager conforms to both NetworkManaging and NetworkStreaming. Existing mocks of NetworkManaging are unaffected.
public struct StreamingResponse: Sendable {
public let statusCode: Int
public let headers: [String: String]
public let bytes: AsyncThrowingStream<UInt8, Error>
public func lines() -> AsyncThrowingStream<String, Error>
public func ndjson<Item: Decodable & Sendable>(
as itemType: Item.Type,
decoder: JSONDecoder = JSONDecoder()
) -> AsyncThrowingStream<Item, Error>
}bytes— raw octet stream, oneUInt8per element, in arrival order.lines()— UTF-8 lines split on\n, trailing\rtrimmed (CRLF-aware), blank lines skipped. Resilient to multi-byte sequences split across TCP segments.ndjson(as:decoder:)— oneDecodablerecord per non-empty line. A bad line throws and finishes the stream.
Cancellation propagates automatically: breaking out of iteration or cancelling the surrounding Task cancels the underlying network task.
public protocol URLSessionStreamingProtocol: Sendable {
func byteStream(for request: URLRequest) async throws -> (AsyncThrowingStream<UInt8, Error>, URLResponse)
}URLSession conforms by default (bridges URLSession.bytes(for:) into a fully Sendable stream). Implement this protocol for unit-testing the streaming pipeline without hitting the network.
| Concern | send(_:) |
stream(_:) |
|---|---|---|
| Headers, body, auth | buildURLRequest |
buildURLRequest (same path) |
| 401 → token refresh + retry | once, when allowsRetry == true |
once, before any body byte is emitted |
| Mid-stream 401 | n/a | not retried (body has already started) |
| Non-2xx error | HTTPError / errorDecoder |
drain ≤1 MiB, then HTTPError / errorDecoder |
Retry policy (RetryPolicy) |
applied | not applied (streams cannot be replayed deterministically) |
Progress (NetworkProgress) |
applied | not applied |
public enum StreamingError: Error, Equatable {
case invalidResponse // response was not an HTTPURLResponse
case errorPayloadTooLarge(limitBytes: Int) // non-2xx body exceeded 1 MiB cap
}struct PlayerSearchRequest: NetworkRequest {
typealias Response = EmptyResponse // unused for streaming
var path: String { "/api/v1/players/search" }
var method: HTTPMethod { .get }
var queryParameters: [String: String]? { ["q": query, "stream": "true"] }
var headers: [String: String]? { DeviceHeaders.current() }
let query: String
}
let response = try await manager.stream(
PlayerSearchRequest(query: "Bobr"),
accessToken: { TokenStore.shared.accessToken }
)
for try await item in response.ndjson(as: SearchEvent.self) {
handle(item) // render incrementally as items arrive
if case .end = item { break }
}let response = try await manager.stream(MyEventsRequest(), accessToken: nil)
for try await line in response.lines() {
guard line.hasPrefix("data:") else { continue }
let payload = line.dropFirst("data:".count).trimmingCharacters(in: .whitespaces)
process(payload)
}Protocol for errors that should not be retried by the default RetryPolicy. Conform your business-level or user-presentable error types to this protocol instead of relying on type name checks.
public protocol NonRetriableError: Error {}Errors that can occur during network operations.
public enum NetworkError: Error {
case invalidURL // URL could not be constructed (e.g. path contains "..")
case invalidMultipartEncoding // Multipart form data failed to encode (e.g. non-UTF-8 header values)
case emptyResponse // Response data was empty
case unauthorized // Unauthorized access, typically HTTP 401
case invalidResponse // Response was missing or of an unexpected type
case conflictingBodyTypes // Both body and multipartData are set
}Generic HTTP error carrying status code and payload for diagnostics.
public struct HTTPError: LocalizedError {
public let statusCode: Int
public let data: Data
public let headers: [String: String]
public var errorDescription: String? {
"Request failed with status code \(statusCode)"
}
}Convenience response that only exposes the HTTP status code and headers.
public struct StatusCodeResponse: Decodable, Equatable {
public let statusCode: Int
public let headers: [String: String]
}Use this response when success is conveyed solely through the HTTP metadata. The default emptyResponseHandler for Response == StatusCodeResponse copies the status code and headers from the empty HTTPURLResponse, so you get those values without decoding a body.
Represents an empty payload. Useful for endpoints that only signal success via status code.
public struct EmptyResponse: Decodable, Equatable {
public init() {}
}EmptyResponse is handy for endpoints that return 204/empty bodies. The request's default decodeResponse returns EmptyResponse() immediately and ignores any server payload, so you can treat the response type as a void success marker.
Configuration for User-Agent header generation.
appName: String- Application nameappVersion: String- Application versionbundleIdentifier: String- Bundle identifierbuildNumber: String- Build numberosVersion: String- iOS/OS versionnetworkVersion: String- EKNetwork framework version
public init(
appName: String? = nil,
appVersion: String? = nil,
bundleIdentifier: String? = nil,
buildNumber: String? = nil,
osVersion: String? = nil,
networkVersion: String? = nil
)All parameters are optional and default to values from Bundle.main or system defaults.
Generates the User-Agent string in the format:
AppName/Version (BundleID; build:BuildNumber; Platform OSVersion) EKNetwork/Version
Protocol abstraction for NetworkManager to allow mocking and dependency injection.
public protocol NetworkManaging {
var tokenRefresher: TokenRefreshProvider? { get set }
func send<T: NetworkRequest>(_ request: T, accessToken: (() -> String?)?) async throws -> T.Response
}Protocol abstraction for URLSession to allow mocking and dependency injection.
public protocol URLSessionProtocol {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}URLSession conforms to this protocol by default.
Полная русскоязычная документация API доступна в отдельном файле:
- 📚 API_RU.md - Полный справочник API на русском языке