Generic HTTP Client Library pitch

Hi, everyone!

Building on the original pitch by @tachyonics (Generic HTTP client/server library), I would like to formally pitch an HTTP client library, built on top of SwiftNIO.

API could look like this:

class HTTPClient {

    public func get(url: String, timeout: Timeout? = nil) -> EventLoopFuture<Response> {
    }

    public func post(url: String, body: Body? = nil, timeout: Timeout? = nil) -> EventLoopFuture<Response> {
    }

    public func put(url: String, body: Body? = nil, timeout: Timeout? = nil) -> EventLoopFuture<Response> {
    }

    public func delete(url: String, timeout: Timeout? = nil) -> EventLoopFuture<Response> {
    }

    public func execute(request: Request, timeout: Timeout? = nil) -> EventLoopFuture<Response> {
    }
}

where HTTPClient.Request and HTTPClient.Response are:

extension HTTPClient {
     enum Body {
        case byteBuffer(ByteBuffer)
        case data(Data)
        case string(String)
    }

    struct Request {
        public var version: HTTPVersion
        public var method: HTTPMethod
        public var url: URL
        public var headers: HTTPHeaders
        public var body: Body?
    }

    struct Response {
        public var host: String
        public var status: HTTPResponseStatus
        public var headers: HTTPHeaders
        public var body: ByteBuffer?
    }
}

If one needs greater control over the response, there is HTTPResponseDelegate with the following protocol:

public protocol HTTPClientResponseDelegate: class {
    associatedtype Response

    // this method will be called when request body is sent  
    func didTransmitRequestBody(task: HTTPClient.Task<Response>)

    // this method will be called when we receive Head response, with headers and status code
    func didReceiveHead(task: HTTPClient.Task<Response>, _ head: HTTPResponseHead)

    // this method will be called multiple times with chunks of the HTTP response body (if there is a body)
    func didReceivePart(task: HTTPClient.Task<Response>, _ buffer: ByteBuffer)

    // this method will be called if an error occurs during request processing
    func didReceiveError(task: HTTPClient.Task<Response>, _ error: Error)

    // this will be called when HTTP response is read fully
    func didFinishRequest(task: HTTPClient.Task<Response>) throws -> Response
}

extension HTTPClient {
    public func execute<T: HTTPClientResponseDelegate>(request: Request, delegate: T, timeout: Timeout? = nil) -> Task<T.Response> {
    }
}

(Task is just a wrapper for EventLoopFuture that in addition to that future provides a way to cancel current request)

To make this proposal concrete, we've built first iteration of such a client:

This client supports the following:

  1. Asynchronous and non-blocking request methods
  2. Simple follow-redirects (cookie headers are dropped)
  3. Streaming body download
  4. TLS support
  5. Cookie parsing (but not storage)

What kind of API would you like to see? What functionality would be sufficient to call this 1.0.0?

We also hope that this project would be mainly SSWG/community-driven, and not by just by one party.

23 Likes

Thank you for this, +1 for me, this is great to have a higher level HTTP client the whole community can invest time on!

I created an issue to discuss ideas: Improvement ideas · Issue #21 · swift-server/async-http-client · GitHub

About what's needed for tagging it 1.0.0, I already love how the API is and it seems really capable already. I hope that cycle detection would be implemented and minimal redirection options (in particular max redirection count) as this is important to avoid infinite loops.
If this is tagged 1.0.0, does this somehow lock us from releasing a 2.0.0 in the really near future, or are we free from concerns about quick API evolution for the package users?

@adtrevor for full details on how sswg libraries evolve, check out our sswg incubation process. additionally, libraries should be semantically versioned, but there is no direct correlation between the semantic version and the maturity phase. i.e. you dont need to wait with 1.0.0 until you are graduated, and the version is purely a function of the API evolution

with that said, in previous cases of new, sswg initiated, libraries (like swift-log, swift-metrics and this one), we waited with initial tagging until after the vote to accept the library into the incubation phase, leveraging the pitch & proposal phases to provide feedback driving the evolution of initial API. as such, we are not ready to tag this one yet, but that would come soon enough

Thank you for your feedback!

It doesn't locks us from tagging 2.0.0, but I want to have feedback on the streaming API in particular first, before tagging 1.0.0

Looks great! Of interest to us would be support for MTLS.

Great idea, filed https://github.com/swift-server/swift-nio-http-client/issues/27,
thanks!

Just to make sure, do you mean Mutual TLS or Multiplexed TLS?

Hi Artem,

Thanks for your replies and filing an issue.

Definitely mutual TLS (draft-ietf-oauth-mtls-03) is what we are interested in.

We are actually particularly interested in trying this client library with a Swagger 2.0 API. It just happens that one of the main Swagger API client code generation tools currently is thinking of creating a new Swift 5 code generation template with more general HTTP client support. I have actually suggested this new client library be supported - see my comment: New Generator: Swift5 client generator with multiple HTTP library support · Issue #2721 · OpenAPITools/openapi-generator · GitHub. Not sure if it's in your remit to get involved in such things but if done well might allow e.g. easy side-by-side comparison of different HTTP clients against the same Swagger API, great for testing etc.

I run a startup working on a new mobile app and we are keen to incrementally migrate to Swift on our backend to amongst other things share a single Swift data schema between mobile app and backend.

BR,
Mark

1 Like

Is this thread still an appropriate place for high level API comments, or should issues on the repo be used for such discussion?

Hi, I've just created a proposal for the client: [Discussion] NIO-based HTTP Client, I think that proposal thread would be the best place to continue discussions about API at this point.

This is already supported.

let’s have that conversation on the proposal