A modern and lightweight Swift HTTP client featuring Swift 6 actor isolation, async/await, cache support, and ergonomic request building.
- Actor-isolated for safe concurrency (Swift 6 actors)
- Fully async/await API
- First-class request & response modeling
- Built-in cache support (URLCache)
- Automatic URL normalization
- Simple, ergonomic API for all HTTP verbs
- Fully Tested
Add SwiftHTTPClient to your dependencies in Package.swift:
.package(url: "https://github.com/joshgallantt/SwiftHTTPClient.git", from: "1.0.0")
Or via Xcode: File > Add Packages… and search for this repo URL.
Import the package:
import SwiftHTTPClient
Create an HTTP client instance:
let client = SwiftHTTPClient(host: "https://api.example.com")
Perform a GET request:
let result = await client.get("/users/42")
Handle the result:
switch result {
case .success(let response):
print(String(data: response.data, encoding: .utf8) ?? "")
print("Status code:", response.response.statusCode)
case .failure(let error):
print("Request failed:", error)
}
POST with an Encodable body:
struct Item: Encodable {
let name: String
}
let result = await client.post("/items", body: Item(name: "Swift"))
POST with Data Payload:
let payload = "field1=value1&field2=value2".data(using: .utf8)!
let result = await client.post(
"/submit",
headers: ["Content-Type": "application/x-www-form-urlencoded"],
data: payload
)
SwiftHTTPClient uses URLCache
under the hood and supports cache policy overrides per request.
When initializing the SwiftHTTPClient
, you can specify a default URLRequest.CachePolicy
. This value applies to all requests unless explicitly overridden.
let client = SwiftHTTPClient(
host: "https://api.example.com",
defaultCachePolicy: .returnCacheDataElseLoad
)
You can override the cache policy for any individual request:
let result = await client.get(
"/weather/today",
cachePolicy: .reloadIgnoringLocalCacheData
)
This allows for fine-grained control without affecting the global configuration.
Set commonHeaders when creating your client to send headers with every request.
Use the headers parameter to override or add headers for a single request. If a key appears in both, the per-request header wins.
let client = SwiftHTTPClient(
host: "https://api.example.com",
commonHeaders: [
"Content-Type": "application/json",
"X-Client-Version": "1.0"
]
)
let result = await client.post(
"/upload",
headers: [
"Content-Type": "image/png", // overrides common header
"X-Request-ID": "abc-123" // adds an extra header
],
data: imageData
)
You can add query parameters and URL fragments to any request.
Use the queryItems parameter to pass query parameters as a [String: String]
dictionary.
Use the fragment parameter to append a fragment to the URL.
let result = await client.get(
"/search",
queryItems: [
"q": "swift",
"limit": "10"
],
fragment: "section2"
)
This produces a request to:
https://api.example.com/search?q=swift&limit=10#section2
All SwiftHTTPClient methods return a Result<HTTPSuccess, HTTPFailure>
.
Represents a successful HTTP response (2xx). Contains:
struct HTTPSuccess: Sendable {
let data: Data
let response: HTTPURLResponse
}
- data — the response body as Data
- response — the HTTPURLResponse (status code, headers, etc)
Represents a failed request. Possible cases:
enum HTTPFailure: Error, CustomStringConvertible, Sendable {
case invalidURL
case server(statusCode: Int, data: Data?)
case invalidResponse
case transport(Error)
case encoding(Error)
}
- .invalidURL — The URL is invalid or could not be constructed.
- .server(statusCode:data:) — The server returned a non-2xx status code (with optional error body).
- .invalidResponse — Response was missing or malformed.
- .transport(Error) — A transport-layer error occurred (such as network down, timeout).
- .encoding(Error) — Failed to encode the request body.
All HTTPFailure values have a human-readable .description for easy logging.
let result = await client.get("/resource")
switch result {
case .success(let output):
print("Data: \(output.data)")
print("Status code: \(output.response.statusCode)")
case .failure(let error):
print("Request failed: \(error.description)")
}
To write unit tests for code that depends on network requests, inject the protocol HTTPClient
instead of using SwiftHTTPClient
directly.
This makes your repositories and services easy to test, and allows you to use the provided MockHTTPClient
in your test target.
import SwiftHTTPClient
final class UserRepository {
private let httpClient: HTTPClient
// Dependency injection via initializer
init(httpClient: HTTPClient) {
self.httpClient = httpClient
}
func fetchUser(id: String) async -> String? {
let result = await httpClient.get("/users/\(id)")
switch result {
case .success(let success):
return String(data: success.data, encoding: .utf8)
case .failure:
return nil
}
}
}
MockHTTPClient
is included in the main target so you can use it from your app or library’s own test code.
import XCTest
import SwiftHTTPClient
final class UserRepositoryTests: XCTestCase {
func test_givenValidUserId_whenFetchUser_thenReturnsUserString() async {
// Given
let mock = MockHTTPClient()
let expectedData = Data("Jane Doe".utf8)
let response = HTTPURLResponse(
url: URL(string: "https://api.example.com/users/42")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
await mock.setGetResult(.success(HTTPSuccess(data: expectedData, response: response)))
let repo = UserRepository(httpClient: mock)
// When
let result = await repo.fetchUser(id: "42")
let calls = await mock.recordedCalls
// Then
XCTAssertEqual(result, "Jane Doe")
XCTAssertEqual(
calls,
[.get(path: "/users/42", headers: nil, queryItems: nil, fragment: nil)]
)
}
func test_givenServerFailure_whenFetchUser_thenReturnsNil() async {
// Given
let mock = MockHTTPClient()
await mock.setGetResult(.failure(.server(statusCode: 500, data: nil)))
let repo = UserRepository(httpClient: mock)
// When
let result = await repo.fetchUser(id: "500")
// Then
XCTAssertNil(result)
}
}
- Fast, reliable tests: Your tests run without making real HTTP calls.
- Easy assertions: You can check what requests were made using
mock.recordedCalls
. - No global state: Each test uses its own mock and does not affect others.
Tip:
You can inject either SwiftHTTPClient
for real networking, or MockHTTPClient
for tests—just by using the protocol.
Created by Josh Gallant. MIT License.