Skip to content

Add HTTPClientReuqest.Prepared #511

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 3 commits into from
Dec 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

#if compiler(>=5.5) && canImport(_Concurrency)
import NIOHTTP1

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension HTTPClientRequest {
struct Prepared {
var poolKey: ConnectionPool.Key
var requestFramingMetadata: RequestFramingMetadata
var head: HTTPRequestHead
var body: Body?
}
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension HTTPClientRequest.Prepared {
init(_ request: HTTPClientRequest) throws {
let url = try DeconstructedURL(url: request.url)

var headers = request.headers
headers.addHostIfNeeded(for: url)
let metadata = try headers.validateAndSetTransportFraming(
method: request.method,
bodyLength: .init(request.body)
)

self.init(
poolKey: .init(url: url, tlsConfiguration: nil),
requestFramingMetadata: metadata,
head: .init(
version: .http1_1,
method: request.method,
uri: url.uri,
headers: headers
),
body: request.body
)
}
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
extension RequestBodyLength {
init(_ body: HTTPClientRequest.Body?) {
switch body?.mode {
case .none:
self = .fixed(length: 0)
case .byteBuffer(let buffer):
self = .fixed(length: buffer.readableBytes)
case .sequence(nil, _), .asyncSequence(nil, _):
self = .dynamic
case .sequence(.some(let length), _), .asyncSequence(.some(let length), _):
self = .fixed(length: length)
}
}
}

#endif
35 changes: 29 additions & 6 deletions Sources/AsyncHTTPClient/ConnectionPool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
//
//===----------------------------------------------------------------------===//

import NIOSSL

enum ConnectionPool {
/// Used by the `ConnectionPool` to index its `HTTP1ConnectionProvider`s
///
Expand All @@ -23,12 +25,14 @@ enum ConnectionPool {
var connectionTarget: ConnectionTarget
private var tlsConfiguration: BestEffortHashableTLSConfiguration?

init(_ request: HTTPClient.Request) {
self.scheme = request.deconstructedURL.scheme
self.connectionTarget = request.deconstructedURL.connectionTarget
if let tls = request.tlsConfiguration {
self.tlsConfiguration = BestEffortHashableTLSConfiguration(wrapping: tls)
}
init(
scheme: Scheme,
connectionTarget: ConnectionTarget,
tlsConfiguration: BestEffortHashableTLSConfiguration? = nil
) {
self.scheme = scheme
self.connectionTarget = connectionTarget
self.tlsConfiguration = tlsConfiguration
}

var description: String {
Expand All @@ -48,3 +52,22 @@ enum ConnectionPool {
}
}
}

extension ConnectionPool.Key {
init(url: DeconstructedURL, tlsConfiguration: TLSConfiguration?) {
self.init(
scheme: url.scheme,
connectionTarget: url.connectionTarget,
tlsConfiguration: tlsConfiguration.map {
BestEffortHashableTLSConfiguration(wrapping: $0)
}
)
}

init(_ request: HTTPClient.Request) {
self.init(
url: request.deconstructedURL,
tlsConfiguration: request.tlsConfiguration
)
}
}
22 changes: 22 additions & 0 deletions Sources/AsyncHTTPClient/ConnectionTarget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,25 @@ enum ConnectionTarget: Equatable, Hashable {
}
}
}

extension ConnectionTarget {
/// The host name which will be send as an HTTP `Host` header.
/// Only returns nil if the `self` is a `unixSocket`.
var host: String? {
switch self {
case .ipAddress(let serialization, _): return serialization
case .domain(let name, _): return name
case .unixSocket: return nil
}
}

/// The host name which will be send as an HTTP host header.
/// Only returns nil if the `self` is a `unixSocket`.
var port: Int? {
switch self {
case .ipAddress(_, let address): return address.port!
case .domain(_, let port): return port
case .unixSocket: return nil
}
}
}
7 changes: 7 additions & 0 deletions Sources/AsyncHTTPClient/DeconstructedURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ struct DeconstructedURL {
}

extension DeconstructedURL {
init(url: String) throws {
guard let url = URL(string: url) else {
throw HTTPClientError.invalidURL
}
try self.init(url: url)
}

init(url: URL) throws {
guard let schemeString = url.scheme else {
throw HTTPClientError.emptyScheme
Expand Down
21 changes: 3 additions & 18 deletions Sources/AsyncHTTPClient/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,20 +200,12 @@ extension HTTPClient {

/// Remote host, resolved from `URL`.
public var host: String {
switch self.deconstructedURL.connectionTarget {
case .ipAddress(let serialization, _): return serialization
case .domain(let name, _): return name
case .unixSocket: return ""
}
self.deconstructedURL.connectionTarget.host ?? ""
}

/// Resolved port.
public var port: Int {
switch self.deconstructedURL.connectionTarget {
case .ipAddress(_, let address): return address.port!
case .domain(_, let port): return port
case .unixSocket: return self.deconstructedURL.scheme.defaultPort
}
self.deconstructedURL.connectionTarget.port ?? self.deconstructedURL.scheme.defaultPort
}

/// Whether request will be executed using secure socket.
Expand All @@ -227,14 +219,7 @@ extension HTTPClient {
headers: self.headers
)

if !head.headers.contains(name: "host") {
let port = self.port
var host = self.host
if !(port == 80 && self.deconstructedURL.scheme == .http), !(port == 443 && self.deconstructedURL.scheme == .https) {
host += ":\(port)"
}
head.headers.add(name: "host", value: host)
}
head.headers.addHostIfNeeded(for: self.deconstructedURL)

let metadata = try head.headers.validateAndSetTransportFraming(method: self.method, bodyLength: .init(self.body))

Expand Down
17 changes: 17 additions & 0 deletions Sources/AsyncHTTPClient/RequestValidation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,20 @@ extension HTTPHeaders {
}
}
}

extension HTTPHeaders {
mutating func addHostIfNeeded(for url: DeconstructedURL) {
// if no host header was set, let's use the url host
guard !self.contains(name: "host"),
var host = url.connectionTarget.host
else {
return
}
// if the request uses a non-default port, we need to add it after the host
if let port = url.connectionTarget.port,
port != url.scheme.defaultPort {
host += ":\(port)"
}
self.add(name: "host", value: host)
}
}