Skip to content

Support UNIX Domain Sockets #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 27, 2020
15 changes: 11 additions & 4 deletions Sources/AsyncHTTPClient/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -281,15 +281,22 @@ public class HTTPClient {
bootstrap = bootstrap.connectTimeout(timeout)
}

let address = self.resolveAddress(request: request, proxy: self.configuration.proxy)
bootstrap.connect(host: address.host, port: address.port)
.map { channel in
task.setChannel(channel)
let eventLoopChannel: EventLoopFuture<Channel>
if request.kind == .unixSocket, let baseURL = request.url.baseURL {
eventLoopChannel = bootstrap.connect(unixDomainSocketPath: baseURL.path)
} else {
let address = self.resolveAddress(request: request, proxy: self.configuration.proxy)
eventLoopChannel = bootstrap.connect(host: address.host, port: address.port)
}

eventLoopChannel.map { channel in
task.setChannel(channel)
}
.flatMap { channel in
channel.writeAndFlush(request)
}
.cascadeFailure(to: task.promise)

return task
}

Expand Down
56 changes: 39 additions & 17 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,32 @@ extension HTTPClient {

/// Represent HTTP request.
public struct Request {

/// Represent kind of Request
enum Kind {
/// Remote host request.
case host
/// UNIX Domain Socket HTTP request.
case unixSocket

func isSchemeSupported(scheme: String) -> Bool {
switch self {
case .host:
return scheme == "http" || scheme == "https"
case .unixSocket:
return scheme == "unix"
}
}
}

/// Request HTTP method, defaults to `GET`.
public let method: HTTPMethod
public var method: HTTPMethod
/// Remote URL.
public let url: URL
public var url: URL
/// Remote HTTP scheme, resolved from `URL`.
public let scheme: String
public var scheme: String
/// Remote host, resolved from `URL`.
public let host: String
public var host: String
/// Request custom HTTP Headers, defaults to no headers.
public var headers: HTTPHeaders
/// Request body, defaults to no body.
Expand All @@ -107,6 +125,7 @@ extension HTTPClient {
}

var redirectState: RedirectState?
let kind: Kind

/// Create HTTP request.
///
Expand All @@ -133,7 +152,6 @@ extension HTTPClient {
///
/// - parameters:
/// - url: Remote `URL`.
/// - version: HTTP version.
/// - method: HTTP method.
/// - headers: Custom HTTP headers.
/// - body: Request body.
Expand All @@ -146,22 +164,26 @@ extension HTTPClient {
throw HTTPClientError.emptyScheme
}

guard Request.isSchemeSupported(scheme: scheme) else {
throw HTTPClientError.unsupportedScheme(scheme)
}
if Kind.host.isSchemeSupported(scheme: scheme) {
self.kind = .host
guard let host = url.host else {
throw HTTPClientError.emptyHost
}

guard let host = url.host else {
throw HTTPClientError.emptyHost
self.host = host
} else if Kind.unixSocket.isSchemeSupported(scheme: scheme) {
self.kind = .unixSocket
self.host = ""
} else {
throw HTTPClientError.unsupportedScheme(scheme)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block can be rewritten to be substantially cleaner by defining some helpers:

extension Kind {
    private static var hostSchemes = ["http", "https"]
    private static var unixSchemes = ["unix"]

    init(forScheme scheme: String) throws {
        if Kind.hostSchemes.contains(scheme) {
            self = .host
        } else if Kind.unixSchemes.contains(scheme) {
            self = .unixSocket
        } else {
            throw HTTPClientError.unsupportedScheme
        }
    }

    func hostFromURL(_ url: URL) throws -> String {
        switch self {
        case .host:
            guard let host = url.host else {
                throw HTTPClientError.emptyHost
            }
            return host
        case .unixSocket:
            return ""
        }
    }
}

This code then becomes:

self.kind = try Kind(scheme: scheme)
self.host = try self.kind.hostFromURL(url)

We can then also rewrite kind.isSchemeSupported in terms of our statics. This should put most of the complexity into the enum definition, which helps keep the code elsewhere a lot nicer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the feedback. Applied as requested.


self.method = method
self.redirectState = nil
self.url = url
self.method = method
self.scheme = scheme
self.host = host
self.headers = headers
self.body = body

self.redirectState = nil
}

/// Whether request will be executed using secure socket.
Expand All @@ -174,8 +196,8 @@ extension HTTPClient {
return self.url.port ?? (self.useTLS ? 443 : 80)
}

static func isSchemeSupported(scheme: String) -> Bool {
return scheme == "http" || scheme == "https"
func isSchemeSupported(scheme: String) -> Bool {
return kind.isSchemeSupported(scheme: scheme)
}
}

Expand Down Expand Up @@ -812,7 +834,7 @@ internal struct RedirectHandler<ResponseType> {
return nil
}

guard HTTPClient.Request.isSchemeSupported(scheme: self.request.scheme) else {
guard request.isSchemeSupported(scheme: self.request.scheme) else {
return nil
}

Expand Down
12 changes: 10 additions & 2 deletions Tests/AsyncHTTPClientTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,21 @@ class HTTPClientTests: XCTestCase {

let request2 = try Request(url: "https://someserver.com")
XCTAssertEqual(request2.url.path, "")

let request3 = try Request(url: "unix:///tmp/file")
XCTAssertNil(request3.url.host)
XCTAssertEqual(request3.host, "")
XCTAssertEqual(request3.url.path, "/tmp/file")
XCTAssertEqual(request3.port, 80)
XCTAssertFalse(request3.useTLS)
}

func testBadRequestURI() throws {
XCTAssertThrowsError(try Request(url: "some/path"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyScheme)
}
XCTAssertThrowsError(try Request(url: "file://somewhere/some/path?foo=bar"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.unsupportedScheme("file"))
XCTAssertThrowsError(try Request(url: "app://somewhere/some/path?foo=bar"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.unsupportedScheme("app"))
}
XCTAssertThrowsError(try Request(url: "https:/foo"), "should throw") { error in
XCTAssertEqual(error as! HTTPClientError, HTTPClientError.emptyHost)
Expand All @@ -63,6 +70,7 @@ class HTTPClientTests: XCTestCase {

func testSchemaCasing() throws {
XCTAssertNoThrow(try Request(url: "hTTpS://someserver.com:8888/some/path?foo=bar"))
XCTAssertNoThrow(try Request(url: "uNIx:///some/path"))
}

func testGet() throws {
Expand Down