Code Monkey home page Code Monkey logo

async-http-client's Introduction

AsyncHTTPClient

This package provides an HTTP Client library built on top of SwiftNIO.

This library provides the following:

  • First class support for Swift Concurrency (since version 1.9.0)
  • Asynchronous and non-blocking request methods
  • Simple follow-redirects (cookie headers are dropped)
  • Streaming body download
  • TLS support
  • Automatic HTTP/2 over HTTPS (since version 1.7.0)
  • Cookie parsing (but not storage)

NOTE: You will need Xcode 13.2 or Swift 5.5.2 to try out AsyncHTTPClients new async/await APIs.


Getting Started

Adding the dependency

Add the following entry in your Package.swift to start using HTTPClient:

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

and AsyncHTTPClient dependency to your target:

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

Request-Response API

The code snippet below illustrates how to make a simple GET request to a remote server.

Please note that the example will spawn a new EventLoopGroup which will create fresh threads which is a very costly operation. In a real-world application that uses SwiftNIO for other parts of your application (for example a web server), please prefer eventLoopGroupProvider: .shared(myExistingEventLoopGroup) to share the EventLoopGroup used by AsyncHTTPClient with other parts of your application.

If your application does not use SwiftNIO yet, it is acceptable to use eventLoopGroupProvider: .createNew but please make sure to share the returned HTTPClient instance throughout your whole application. Do not create a large number of HTTPClient instances with eventLoopGroupProvider: .createNew, this is very wasteful and might exhaust the resources of your program.

import AsyncHTTPClient

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)

/// MARK: - Using Swift Concurrency
let request = HTTPClientRequest(url: "https://apple.com/")
let response = try await httpClient.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
}


/// MARK: - Using SwiftNIO EventLoopFuture
httpClient.get(url: "https://apple.com/").whenComplete { result in
    switch result {
    case .failure(let error):
        // process error
    case .success(let response):
        if response.status == .ok {
            // handle response
        } else {
            // handle remote error
        }
    }
}

You should always shut down HTTPClient instances you created using try httpClient.syncShutdown(). Please note that you must not call httpClient.syncShutdown before all requests of the HTTP client have finished, or else the in-flight requests will likely fail because their network connections are interrupted.

async/await examples

Examples for the async/await API can be found in the Examples folder in this Repository.

Usage guide

The default HTTP Method is GET. In case you need to have more control over the method, or you want to add headers or body, use the HTTPClientRequest struct:

Using Swift Concurrency

import AsyncHTTPClient

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
do {
    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.execute(request, timeout: .seconds(30))
    if response.status == .ok {
        // handle response
    } else {
        // handle remote error
    }
} catch {
    // handle error
}
// it's important to shutdown the httpClient after all requests are done, even if one failed
try await httpClient.shutdown()

Using SwiftNIO EventLoopFuture

import AsyncHTTPClient

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
defer {
    try? httpClient.syncShutdown()
}

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.execute(request: request).whenComplete { result in
    switch result {
    case .failure(let error):
        // process error
    case .success(let response):
        if response.status == .ok {
            // handle response
        } else {
            // handle remote error
        }
    }
}

Redirects following

Enable follow-redirects behavior using the client configuration:

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

Timeouts

Timeouts (connect and read) can also be set using the client configuration:

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

or on a per-request basis:

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

Streaming

When dealing with larger amount of data, it's critical to stream the response body instead of aggregating in-memory. The following example demonstrates how to count the number of bytes in a streaming response body:

Using Swift Concurrency

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
do {
    let request = HTTPClientRequest(url: "https://apple.com/")
    let response = try await httpClient.execute(request, timeout: .seconds(30))
    print("HTTP head", response)

    // if defined, the content-length headers announces the size of the body
    let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init)

    var receivedBytes = 0
    // asynchronously iterates over all body fragments
    // this loop will automatically propagate backpressure correctly
    for try await buffer in response.body {
        // for this example, we are just interested in the size of the fragment
        receivedBytes += buffer.readableBytes
        
        if let expectedBytes = expectedBytes {
            // if the body size is known, we calculate a progress indicator
            let progress = Double(receivedBytes) / Double(expectedBytes)
            print("progress: \(Int(progress * 100))%")
        }
    }
    print("did receive \(receivedBytes) bytes")
} catch {
    print("request failed:", error) 
}
// it is important to shutdown the httpClient after all requests are done, even if one failed 
try await httpClient.shutdown()

Using HTTPClientResponseDelegate and SwiftNIO EventLoopFuture

import NIOCore
import NIOHTTP1

class CountingDelegate: HTTPClientResponseDelegate {
    typealias Response = Int

    var count = 0

    func didSendRequestHead(task: HTTPClient.Task<Response>, _ head: HTTPRequestHead) {
        // this is executed right after request head was sent, called once
    }

    func didSendRequestPart(task: HTTPClient.Task<Response>, _ part: IOData) {
        // this is executed when request body part is sent, could be called zero or more times
    }

    func didSendRequest(task: HTTPClient.Task<Response>) {
        // this is executed when request is fully sent, called once
    }

    func didReceiveHead(
        task: HTTPClient.Task<Response>, 
        _ head: HTTPResponseHead
    ) -> EventLoopFuture<Void> {
        // this is executed when we receive HTTP response head part of the request 
        // (it contains response code and headers), called once in case backpressure 
        // is needed, all reads will be paused until returned future is resolved
        return task.eventLoop.makeSucceededFuture(())
    }

    func didReceiveBodyPart(
        task: HTTPClient.Task<Response>, 
        _ buffer: ByteBuffer
    ) -> EventLoopFuture<Void> {
        // this is executed when we receive parts of the response body, could be called zero or more times
        count += buffer.readableBytes
        // in case backpressure is needed, all reads will be paused until returned future is resolved
        return task.eventLoop.makeSucceededFuture(())
    }

    func didFinishRequest(task: HTTPClient.Task<Response>) throws -> Int {
        // this is called when the request is fully read, called once
        // this is where you return a result or throw any errors you require to propagate to the client
        return count
    }

    func didReceiveError(task: HTTPClient.Task<Response>, _ error: Error) {
        // this is called when we receive any network-related error, called once
    }
}

let request = try HTTPClient.Request(url: "https://apple.com/")
let delegate = CountingDelegate()

httpClient.execute(request: request, delegate: delegate).futureResult.whenSuccess { count in
    print(count)
}

File downloads

Based on the HTTPClientResponseDelegate example above you can build more complex delegates, the built-in FileDownloadDelegate is one of them. It allows streaming the downloaded data asynchronously, while reporting the download progress at the same time, like in the following example:

let client = HTTPClient(eventLoopGroupProvider: .createNew)
let request = try HTTPClient.Request(
    url: "https://swift.org/builds/development/ubuntu1804/latest-build.yml"
)

let delegate = try FileDownloadDelegate(path: "/tmp/latest-build.yml", reportProgress: {
    if let totalBytes = $0.totalBytes {
        print("Total bytes count: \(totalBytes)")
    }
    print("Downloaded \($0.receivedBytes) bytes so far")
})

client.execute(request: request, delegate: delegate).futureResult
    .whenSuccess { progress in
        if let totalBytes = progress.totalBytes {
            print("Final total bytes count: \(totalBytes)")
        }
        print("Downloaded finished with \(progress.receivedBytes) bytes downloaded")
    }

Unix Domain Socket Paths

Connecting to servers bound to socket paths is easy:

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
httpClient.execute(
    .GET, 
    socketPath: "/tmp/myServer.socket", 
    urlPath: "/path/to/resource"
).whenComplete (...)

Connecting over TLS to a unix domain socket path is possible as well:

let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)
httpClient.execute(
    .POST, 
    secureSocketPath: "/tmp/myServer.socket", 
    urlPath: "/path/to/resource", 
    body: .string("hello")
).whenComplete (...)

Direct URLs can easily be constructed to be executed in other scenarios:

let socketPathBasedURL = URL(
    httpURLWithSocketPath: "/tmp/myServer.socket", 
    uri: "/path/to/resource"
)
let secureSocketPathBasedURL = URL(
    httpsURLWithSocketPath: "/tmp/myServer.socket", 
    uri: "/path/to/resource"
)

Disabling HTTP/2

The exclusive use of HTTP/1 is possible by setting httpVersion to .http1Only on HTTPClient.Configuration:

var configuration = HTTPClient.Configuration()
configuration.httpVersion = .http1Only
let client = HTTPClient(
    eventLoopGroupProvider: .createNew,
    configuration: configuration
)

Security

Please have a look at SECURITY.md for AsyncHTTPClient's security process.

async-http-client's People

Contributors

0xtim avatar adam-fowler avatar andrew-lees11 avatar artemredkin avatar cpriebe avatar davidde94 avatar dimitribouniol avatar dnadoba avatar fabianfett avatar franzbusch avatar fundaev avatar glbrntt avatar iainsmith avatar ianpartridge avatar jakepetroules avatar karwa avatar krzyzanowskim avatar ktoso avatar lukasa avatar madsodgaard avatar maxdesiatov avatar t089 avatar tanner0101 avatar tomerd avatar vkill avatar vlm avatar weissi avatar winsmith avatar ya-pulser avatar yasumoto avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.