A HTTP client built with and for PointFree's swift-dependencies
Swift 100%

README.md

HTTPClient #

A lightweight, dependency-injectable HTTP client for Swift applications built on top of Foundation's URLSession and HTTPTypes.

Overview #

HTTPClient provides a clean, testable interface for making HTTP requests with support for request/response interception, automatic retry logic, and comprehensive error handling. It's designed around the swift-dependencies library for easy testing and configuration.

Features #

  • Lightweight: Minimal wrapper around URLSession with HTTPTypes integration
  • Dependency Injectable: Built with swift-dependencies for easy testing and configuration
  • Request/Response Interception: Pluggable interceptors for modifying requests and responses
  • Automatic Retry Logic: Built-in error handling with configurable retry attempts
  • Type-Safe: Full support for Codable types with automatic JSON encoding/decoding
  • Comprehensive HTTP Methods: Support for GET, POST, PUT, and DELETE operations
  • Path-Based API: Convenient methods that work with base URLs and relative paths

Installation #

Add HTTPClient to your Swift package dependencies:

dependencies: [
    .package(url: "https://github.com/woodymelling/swift-dependencies-http-client", from: "1.0.0")
]

Basic Usage #

Making Requests #

import HTTPClient
import Dependencies

// GET request with automatic JSON decoding
let user: User = try await httpClient.get(
    URL(string: "https://api.example.com/users/123")!
)

// POST request with JSON body
let newUser = CreateUserRequest(name: "John", email: "john@example.com")
let createdUser: User = try await httpClient.post(
    URL(string: "https://api.example.com/users")!,
    data: newUser
)

// PUT request
let updatedUser: User = try await httpClient.put(
    URL(string: "https://api.example.com/users/123")!,
    data: updateData
)

// DELETE request
try await httpClient.delete(
    URL(string: "https://api.example.com/users/123")!
)

Path-Based API #

When you have a base URL configured, you can use path-based methods:

// These methods automatically prepend your configured base URL
let user: User = try await httpClient.get("/users/123")
let users: [User] = try await httpClient.get("/users", queryItems: [
    URLQueryItem(name: "page", value: "1")
])

Query Parameters #

let queryItems = [
    URLQueryItem(name: "page", value: "1"),
    URLQueryItem(name: "limit", value: "20")
]

let users: [User] = try await httpClient.get(
    URL(string: "https://api.example.com/users")!,
    queryItems: queryItems
)

Configuration #

Dependency Injection #

HTTPClient is designed to work with the swift-dependencies library:

import Dependencies

// In your app code
@Dependency(\.httpClient) var httpClient

// Configure in tests
withDependencies {
    $0.httpClient = .mock
} operation: {
    // Your test code here
}

Request Interceptors #

Implement custom request interceptors to modify requests before they're sent:

struct AuthorizationInterceptor: RequestInterceptor {
    func run(_ request: inout HTTPRequest, _ body: inout Data?) async throws {
        request.headerFields[.authorization] = "Bearer \(token)"
    }
}

Response Interceptors #

Implement response interceptors to handle responses:

struct LoggingInterceptor: ResponseInterceptor {
    func run(
        _ request: HTTPRequest, 
        _ response: inout HTTPResponse, 
        _ data: inout Data?
    ) async throws {
        print("Response: \(response.status)")
    }
}

Error Handling #

Configure automatic retry logic with error interceptors:

struct RetryInterceptor: ErrorInterceptor {
    let maxRetries = 3
    
    func interceptor(
        _ request: HTTPRequest,
        _ status: HTTPResponse.Status,
        _ data: Data?,
        _ retry: @escaping (HTTPRequest) async throws -> Data?
    ) async throws -> Data? {
        // Custom retry logic
        if status == .serviceUnavailable {
            return try await retry(request)
        }
        throw HTTPError.httpError(status)
    }
}

Testing #

HTTPClient is built for testability. Override the underlying data fetching mechanism in tests:

withDependencies {
    $0.dataForURL = { request, data in
        // Return mock response data
        let mockData = """
        {"id": 1, "name": "Test User"}
        """.data(using: .utf8)!
        
        let response = HTTPResponse(status: .ok)
        return (mockData, response)
    }
} operation: {
    // Your test code here
}

Error Handling #

HTTPClient throws HTTPError for various error conditions:

  • HTTPError.httpError(status): For non-successful HTTP status codes
  • HTTPError.expectedDataResponse: When a GET request returns no data

Dependencies #

This library depends on:

  • Foundation: Core Swift framework
  • HTTPTypes: Apple's HTTP types library
  • HTTPTypesFoundation: Foundation integration for HTTPTypes
  • Dependencies: Point-Free's dependency injection library
  • OSLog: For internal logging

Requirements #

  • Swift 5.9+
  • iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+

License #

[Your License Here]

Contributing #

[Contributing guidelines here]