Library / SDK
swift-server/async-http-client avatar
swift-server/async-http-client

AsyncHTTPClient: an HTTP client for services that already run on SwiftNIO

HTTP client library built on SwiftNIO

1,080 stars156 forksSwiftApache-2.0

At a glance

What is it?
AsyncHTTPClient is the HTTP client from the swift-server group, built on SwiftNIO, offering both async/await and EventLoopFuture APIs with streaming bodies, timeouts and TLS. It is a good fit for a service whose event loop already owns the process, and a poor fit for code that just needs to fetch a URL from a command line tool.
Who is it for?
AsyncHTTPClient fits a Swift service that is built on SwiftNIO, where the client should share the event loop rather than run a separate networking stack, and where a streaming body with real backpressure matters more than a convenient one-line fetch. It does not fit a script that just needs one GET, a client that must keep cookies across redirects, or a codebase that would rather inherit the platform's networking stack.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 16 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What AsyncHTTPClient is for

AsyncHTTPClient is an HTTP client library built on SwiftNIO, from the swift-server organisation, whose other members are the same group that maintains the NIO family. The README lists what it provides: first class support for Swift Concurrency, asynchronous and non-blocking request methods, simple follow-redirects, streaming body download, TLS support, automatic HTTP/2 over HTTPS, and cookie parsing without cookie storage.

The audience is a Swift server developer. The interesting decision this library makes for you is that outgoing HTTP traffic runs on the same SwiftNIO machinery as your server, which means it shares the event loop, the threads and the connection lifecycle discipline you already have. A client that runs on a separate networking stack inside the same process brings its own thread pool and its own failure modes, and that is a cost the swift-server group has designed away.

The scope is worth being clear about, because it is narrow in a useful way. This is a client, not a framework: no caching layer, no retry policy, no circuit breaker, no middleware pipeline, no cookie jar. What you get is a request, a response, a body you can stream, and configuration for the parts of HTTP that are easy to get subtly wrong.

The repository matches that shape. Sources/ holds the implementation, Tests/ the test suite, Examples/ three runnable sample projects, and there are dev/, docs/ and scripts/ directories along with a Package.swift manifest, a .swift-format configuration, a CODE_OF_CONDUCT.md, a SECURITY.md and a NOTICE.txt.

Adding the dependency and making a first request

The dependency is a Swift Package Manager entry, and the README shows exactly what to add to Package.swift:

swift
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0")

and then the product dependency on your target:

swift
.target(name: "MyApp", dependencies: [.product(name: "AsyncHTTPClient", package: "async-http-client")]),

With that in place, a first request is short. The README's Swift Concurrency example builds an HTTPClientRequest, executes it against the shared client with a timeout, and checks the status before touching the body:

swift
import AsyncHTTPClient

let request = HTTPClientRequest(url: "https://apple.com/")
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
print("HTTP head", response)
if response.status == .ok {
    let body = try await response.body.collect(upTo: 1024 * 1024) // 1 MB
    // handle body
} else {
    // handle remote error
}

The collect(upTo:) call is the small detail worth pausing on. It caps how much of the body will be buffered, so a server that sends more than the limit does not quietly fill your heap. The README notes that the Examples folder in the repository holds more async/await samples, and the repository listing shows three of them: GetHTML, GetJSON and StreamingByteCounter, each with its own Package.swift under Examples.

What you should see is a status, and a body you can read. If the body is empty, the request itself succeeded and the problem is on the server side, so check the status code before suspecting the client.

Two request styles, and why the older one is still there

The library exposes the same capability through two interfaces, and the README documents both. The async/await style uses HTTPClientRequest, a value type you mutate to set the method, headers and body:

swift
var request = HTTPClientRequest(url: "https://apple.com/")
request.method = .POST
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
request.body = .bytes(ByteBuffer(string: "some data"))

let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))

The older style uses the SwiftNIO EventLoopFuture API, where the request is a class instance you configure and the result arrives in a completion handler:

swift
var request = try HTTPClient.Request(url: "https://apple.com/", method: .POST)
request.headers.add(name: "User-Agent", value: "Swift HTTPClient")
request.body = .string("some-body")

HTTPClient.shared.execute(request: request).whenComplete { result in
    // process error or handle response
}

The bodies differ in form rather than in capability. The Future version throws on construction, the struct version does not, and the struct version carries the body as .bytes or .string while the class version has a body property assigned directly. Both reach the same response type, and both give you the status and the body.

For new code the async/await path is the natural one. The EventLoopFuture path matters for a different reason: a service already written against NIO can keep its existing call style, and a code base that already has a promise-based error-handling pattern does not have to introduce a second one. The two APIs also differ in lifetime rules, which is the subject of the next section.

Client lifetime, the shared instance, and the shutdown rule

The README is unusually firm about one thing, and the firmness is justified: "If you create your own HTTPClient instances, you should shut them down using httpClient.shutdown() when you're done using them. Failing to do so will leak resources."

The shared instance, HTTPClient.shared, is the simple path. The README states that it follows redirects by default, and for a one-off request it is the right choice because there is no lifecycle to manage. When you need different behaviour, you create your own client with a configuration:

swift
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton,
                            configuration: HTTPClient.Configuration(followRedirects: true))

The eventLoopGroupProvider parameter is how you decide where the client's work happens. The .singleton form shares one event loop group, which is the sensible choice for a service that already has NIO running, and the alternative is supplying your own group, which is what you would do in a service with a specific threading model.

There is a second half to the shutdown rule, and it is the part that bites. The README warns that you must not call httpClient.shutdown before all requests of the HTTP client have finished, because the in-flight requests will likely fail since their network connections are interrupted. Shutting down a client while a request is in flight is therefore not a slow shutdown, it is a broken request.

That is the shape of the whole design. The client owns connections, the connections outlive individual requests, and the lifetime of the client is the lifetime of the resources. A service that creates a client per request will spend its time paying for connection setup and will eventually leak the resources the README warns about.

Redirects, cookies and HTTP/2: what the client handles and what it drops

The redirect support is described as simple, and the parenthetical in the README is the important part: cookie headers are dropped.

So a client that follows a redirect will not carry a Cookie header across it. For a service talking to a public API that redirects a couple of URLs, that is invisible. For anything that relies on a session cookie surviving a redirect, it is a behaviour difference you need to know about before you debug it in production, and the shared client does this by default, so the safe path is to create your own client and decide explicitly.

The cookie story is the same shape. The client parses cookies, but it does not store them, which means there is no cookie jar that automatically attaches a cookie to the next request. A caller who wants session continuity across requests has to carry the cookie itself, either by reading the Set-Cookie header and adding it to the next request, or by wrapping the client in something that does that bookkeeping.

HTTP/2 is automatic over HTTPS, and the word over is doing real work there. The README's list says "Automatic HTTP/2 over HTTPS", which means the negotiation happens on a TLS connection, and the plain HTTP path is HTTP/1.1.

Taken together these are the points where the library stops and your application starts. TLS is handled, protocol negotiation on TLS is handled, redirects are followed with a documented omission, and cookies are parsed but not remembered. The gaps are small and specific, which is easier to work with than a client that does the wrong thing quietly.

Timeouts, streaming bodies and the delegate API

Timeouts are configurable on the client for connect and read separately:

swift
let timeout = HTTPClient.Configuration.Timeout(connect: .seconds(1), read: .seconds(1))
let httpClient = HTTPClient(eventLoopGroupProvider: .singleton,
                            configuration: HTTPClient.Configuration(timeout: timeout))

and per request through a deadline:

swift
httpClient.execute(request: request, deadline: .now() + .milliseconds(1))

The per-request deadline covers the whole operation, while the client-level timeout separates the connect phase from the read phase, which is the distinction that matters when a server accepts a connection and then stalls.

Streaming is where the library earns its place in a server. The README is direct that for larger amounts of data it is critical to stream the response body instead of aggregating it in memory, and the async/await example iterates the body directly:

swift
let response = try await HTTPClient.shared.execute(request, timeout: .seconds(30))
let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init)

var receivedBytes = 0
for try await buffer in response.body {
    receivedBytes += buffer.readableBytes
}

The loop propagates backpressure automatically, which the README calls out in a comment, so a slow consumer slows the read rather than filling a buffer. The content-length lookup is optional, and it exists to turn a byte count into a progress figure.

The NIO-style equivalent is the delegate protocol. A class conforming to HTTPClientResponseDelegate implements didSendRequestHead, didSendRequestPart, didSendRequest, didReceiveHead and didReceiveBodyPart, each returning an EventLoopFuture so that a slow consumer can pause the reads. The README's comment on didReceiveHead explains the contract: all reads are paused until the returned future resolves, and backpressure is requested there.

Versioning, maintenance, and when a different client fits better

The project is not archived, the last push was on 2026-10-07, and the release history is a steady patch line: 1.36.0 on 2026-07-23, 1.36.1 on 2026-09-01 and 1.36.2 on 2026-09-25. Patch releases with no feature changes are the right pattern for a library that many services depend on, because a version bump should not be the reason your build breaks.

The licence is Apache-2.0, with LICENSE.txt and NOTICE.txt in the repository root, and the .licenseignore file suggests licence headers are checked automatically in the source. Apache-2.0 is permissive and includes an express patent grant, which is the reason many companies prefer it over MIT for a dependency in a commercial product.

One documentation detail will bite you if you copy the README literally. The Package.swift snippet uses from: "1.9.0", which resolves to the latest 1.x release, so it will work, but it is well behind the current 1.36.2 and it tells you nothing about which version you actually got. Pin a floor you have tested rather than the number in the README.

The alternative is the one most Swift developers already have: URLSession from Foundation, with its async API. The difference in approach is the entire point. URLSession runs on the platform's networking stack, so it needs no NIO integration and its lifecycle is managed by the system, but you give up the shared event loop with your server, the NIO-native delegate model, and the backpressure-aware streaming loop that the AsyncHTTPClient example demonstrates. A command line tool or a small macOS or iOS app is better served by URLSession. A SwiftNIO service that streams large responses and shares threads with its event loop is better served here.

A third option, writing requests directly against NIOHTTP1, gives complete control and none of the conveniences. It also means reimplementing redirects, TLS configuration and HTTP/2 negotiation, which is exactly the work this library exists to do.

Editorial conclusion

AsyncHTTPClient fits a Swift service that is built on SwiftNIO, where the client should share the event loop rather than run a separate networking stack, and where a streaming body with real backpressure matters more than a convenient one-line fetch. It does not fit a script that just needs one GET, a client that must keep cookies across redirects, or a codebase that would rather inherit the platform's networking stack. Verify first that the version you resolve matches the API you are writing, since the README still shows a from: 1.9.0 constraint while the current release is 1.36.2, and confirm that your redirect-heavy endpoints do not depend on cookies, because the client drops cookie headers when following redirects.

Frequently asked questions

How do I add AsyncHTTPClient to a Swift project?

Add a package entry to Package.swift pointing at https://github.com/swift-server/async-http-client.git, then add the AsyncHTTPClient product to your target's dependencies. The README's example uses .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0") and .product(name: "AsyncHTTPClient", package: "async-http-client").

Do I need to shut down an HTTPClient instance?

Yes. The README states that if you create your own HTTPClient instances you should shut them down with httpClient.shutdown() when done, because failing to do so will leak resources. It also warns that you must not shut down before all requests have finished, since the in-flight requests will likely fail when their network connections are interrupted.

Does AsyncHTTPClient keep cookies between requests?

No. The README lists cookie parsing but not storage, and notes that the follow-redirects behaviour is simple because cookie headers are dropped when a redirect is followed. The globally shared HTTPClient.shared follows redirects by default; a client you create yourself enables it through HTTPClient.Configuration(followRedirects: true).

How do I stream a large response body with AsyncHTTPClient?

Iterate the response body directly with for try await buffer in response.body, which the README says automatically propagates backpressure. The header response.headers.first(name: "content-length") is optional and is used to compute a progress figure. A delegate-based alternative implements HTTPClientResponseDelegate methods such as didReceiveHead and didReceiveBodyPart, returning futures to pause reads.

What licence is AsyncHTTPClient under and how often is it released?

The project is licensed under Apache-2.0, with LICENSE.txt and NOTICE.txt in the repository. The release history shows 1.36.0 on 2026-07-23, 1.36.1 on 2026-09-01 and 1.36.2 on 2026-09-25, and the last push was on 2026-10-07.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. swift-server/async-http-client on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/swift-server-async-http-client.svg)](https://hysenlabs.com/projects/swift-server-async-http-client)

Was this useful?