-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add HTTP types adapter for SwiftNIO (#202)
* Add HTTP types adapter for SwiftNIO * swiftformat * Guard on Swift 5.8 * Review comments * Update swift-http-types to 0.1.1 * Update swift-http-types to 1.0.0 * Review feedback * Review feedback * Bump minimum Swift version to 5.7.1 * Allow Host in any order
- Loading branch information
1 parent
6c3819c
commit 798c962
Showing
12 changed files
with
1,489 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
version: 1 | ||
builder: | ||
configs: | ||
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS] | ||
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import HTTPTypes | ||
import NIOCore | ||
|
||
/// The parts of a complete HTTP request. | ||
/// | ||
/// An HTTP request message is made up of a request encoded by `.head`, zero or | ||
/// more body parts, and optionally some trailers. | ||
/// | ||
/// To indicate that a complete HTTP message has been sent or received, we use | ||
/// `.end`, which may also contain any trailers that make up the message. | ||
public enum HTTPRequestPart: Sendable, Hashable { | ||
case head(HTTPRequest) | ||
case body(ByteBuffer) | ||
case end(HTTPFields?) | ||
} | ||
|
||
/// The parts of a complete HTTP response. | ||
/// | ||
/// An HTTP response message is made up of one or more response headers encoded | ||
/// by `.head`, zero or more body parts, and optionally some trailers. | ||
/// | ||
/// To indicate that a complete HTTP message has been sent or received, we use | ||
/// `.end`, which may also contain any trailers that make up the message. | ||
public enum HTTPResponsePart: Sendable, Hashable { | ||
case head(HTTPResponse) | ||
case body(ByteBuffer) | ||
case end(HTTPFields?) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import HTTPTypes | ||
import NIOCore | ||
import NIOHTTP1 | ||
import NIOHTTPTypes | ||
|
||
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types, | ||
/// and vice versa, for use on the client side. | ||
public final class HTTP1ToHTTPClientCodec: ChannelDuplexHandler { | ||
public typealias InboundIn = HTTPClientResponsePart | ||
public typealias InboundOut = HTTPResponsePart | ||
|
||
public typealias OutboundIn = HTTPRequestPart | ||
public typealias OutboundOut = HTTPClientRequestPart | ||
|
||
/// Initializes a `HTTP1ToHTTPClientCodec`. | ||
public init() {} | ||
|
||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
switch self.unwrapInboundIn(data) { | ||
case .head(let head): | ||
do { | ||
let newResponse = try HTTPResponse(head) | ||
context.fireChannelRead(self.wrapInboundOut(.head(newResponse))) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
} | ||
case .body(let body): | ||
context.fireChannelRead(self.wrapInboundOut(.body(body))) | ||
case .end(let trailers): | ||
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) } | ||
context.fireChannelRead(self.wrapInboundOut(.end(newTrailers))) | ||
} | ||
} | ||
|
||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { | ||
switch self.unwrapOutboundIn(data) { | ||
case .head(let request): | ||
do { | ||
let oldRequest = try HTTPRequestHead(request) | ||
context.write(self.wrapOutboundOut(.head(oldRequest)), promise: promise) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
promise?.fail(error) | ||
} | ||
case .body(let body): | ||
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: promise) | ||
case .end(let trailers): | ||
context.write(self.wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise) | ||
} | ||
} | ||
} | ||
|
||
/// A simple channel handler that translates HTTP/1 messages into shared HTTP types, | ||
/// and vice versa, for use on the server side. | ||
public final class HTTP1ToHTTPServerCodec: ChannelDuplexHandler { | ||
public typealias InboundIn = HTTPServerRequestPart | ||
public typealias InboundOut = HTTPRequestPart | ||
|
||
public typealias OutboundIn = HTTPResponsePart | ||
public typealias OutboundOut = HTTPServerResponsePart | ||
|
||
private let secure: Bool | ||
private let splitCookie: Bool | ||
|
||
/// Initializes a `HTTP1ToHTTPServerCodec`. | ||
/// - Parameters: | ||
/// - secure: Whether "https" or "http" is used. | ||
/// - splitCookie: Whether the cookies received from the server should be split | ||
/// into multiple header fields. Defaults to false. | ||
public init(secure: Bool, splitCookie: Bool = false) { | ||
self.secure = secure | ||
self.splitCookie = splitCookie | ||
} | ||
|
||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
switch self.unwrapInboundIn(data) { | ||
case .head(let head): | ||
do { | ||
let newRequest = try HTTPRequest(head, secure: self.secure, splitCookie: self.splitCookie) | ||
context.fireChannelRead(self.wrapInboundOut(.head(newRequest))) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
} | ||
case .body(let body): | ||
context.fireChannelRead(self.wrapInboundOut(.body(body))) | ||
case .end(let trailers): | ||
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) } | ||
context.fireChannelRead(self.wrapInboundOut(.end(newTrailers))) | ||
} | ||
} | ||
|
||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { | ||
switch self.unwrapOutboundIn(data) { | ||
case .head(let response): | ||
let oldResponse = HTTPResponseHead(response) | ||
context.write(self.wrapOutboundOut(.head(oldResponse)), promise: promise) | ||
case .body(let body): | ||
context.write(self.wrapOutboundOut(.body(.byteBuffer(body))), promise: promise) | ||
case .end(let trailers): | ||
context.write(self.wrapOutboundOut(.end(trailers.map(HTTPHeaders.init))), promise: promise) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftNIO open source project | ||
// | ||
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors | ||
// Licensed under Apache License v2.0 | ||
// | ||
// See LICENSE.txt for license information | ||
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
import HTTPTypes | ||
import NIOCore | ||
import NIOHTTP1 | ||
import NIOHTTPTypes | ||
|
||
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages, | ||
/// and vice versa, for use on the client side. | ||
/// | ||
/// This is intended for compatibility purposes where a channel handler working with | ||
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types | ||
/// abstraction. | ||
public final class HTTPToHTTP1ClientCodec: ChannelDuplexHandler { | ||
public typealias InboundIn = HTTPResponsePart | ||
public typealias InboundOut = HTTPClientResponsePart | ||
|
||
public typealias OutboundIn = HTTPClientRequestPart | ||
public typealias OutboundOut = HTTPRequestPart | ||
|
||
private let secure: Bool | ||
private let splitCookie: Bool | ||
|
||
/// Initializes a `HTTPToHTTP1ClientCodec`. | ||
/// - Parameters: | ||
/// - secure: Whether "https" or "http" is used. | ||
/// - splitCookie: Whether the cookies sent by the client should be split | ||
/// into multiple header fields. Splitting the `Cookie` | ||
/// header field improves the performance of HTTP/2 and | ||
/// HTTP/3 clients by allowing individual cookies to be | ||
/// indexed separately in the dynamic table. It has no | ||
/// effects in HTTP/1. Defaults to true. | ||
public init(secure: Bool, splitCookie: Bool = true) { | ||
self.secure = secure | ||
self.splitCookie = splitCookie | ||
} | ||
|
||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
switch self.unwrapInboundIn(data) { | ||
case .head(let head): | ||
let oldResponse = HTTPResponseHead(head) | ||
context.fireChannelRead(self.wrapInboundOut(.head(oldResponse))) | ||
case .body(let body): | ||
context.fireChannelRead(self.wrapInboundOut(.body(body))) | ||
case .end(let trailers): | ||
context.fireChannelRead(self.wrapInboundOut(.end(trailers.map(HTTPHeaders.init)))) | ||
} | ||
} | ||
|
||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { | ||
switch self.unwrapOutboundIn(data) { | ||
case .head(let request): | ||
do { | ||
let newRequest = try HTTPRequest(request, secure: self.secure, splitCookie: self.splitCookie) | ||
context.write(self.wrapOutboundOut(.head(newRequest)), promise: promise) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
promise?.fail(error) | ||
} | ||
case .body(.byteBuffer(let body)): | ||
context.write(self.wrapOutboundOut(.body(body)), promise: promise) | ||
case .body: | ||
fatalError("File region not supported") | ||
case .end(let trailers): | ||
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) } | ||
context.write(self.wrapOutboundOut(.end(newTrailers)), promise: promise) | ||
} | ||
} | ||
} | ||
|
||
/// A simple channel handler that translates shared HTTP types into HTTP/1 messages, | ||
/// and vice versa, for use on the server side. | ||
/// | ||
/// This is intended for compatibility purposes where a channel handler working with | ||
/// HTTP/1 messages needs to work on top of the new version-independent HTTP types | ||
/// abstraction. | ||
public final class HTTPToHTTP1ServerCodec: ChannelDuplexHandler { | ||
public typealias InboundIn = HTTPRequestPart | ||
public typealias InboundOut = HTTPServerRequestPart | ||
|
||
public typealias OutboundIn = HTTPServerResponsePart | ||
public typealias OutboundOut = HTTPResponsePart | ||
|
||
/// Initializes a `HTTPToHTTP1ServerCodec`. | ||
public init() {} | ||
|
||
public func channelRead(context: ChannelHandlerContext, data: NIOAny) { | ||
switch self.unwrapInboundIn(data) { | ||
case .head(let head): | ||
do { | ||
let oldRequest = try HTTPRequestHead(head) | ||
context.fireChannelRead(self.wrapInboundOut(.head(oldRequest))) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
} | ||
case .body(let body): | ||
context.fireChannelRead(self.wrapInboundOut(.body(body))) | ||
case .end(let trailers): | ||
context.fireChannelRead(self.wrapInboundOut(.end(trailers.map(HTTPHeaders.init)))) | ||
} | ||
} | ||
|
||
public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { | ||
switch self.unwrapOutboundIn(data) { | ||
case .head(let response): | ||
do { | ||
let newResponse = try HTTPResponse(response) | ||
context.write(self.wrapOutboundOut(.head(newResponse)), promise: promise) | ||
} catch { | ||
context.fireErrorCaught(error) | ||
promise?.fail(error) | ||
} | ||
case .body(.byteBuffer(let body)): | ||
context.write(self.wrapOutboundOut(.body(body)), promise: promise) | ||
case .body: | ||
fatalError("File region not supported") | ||
case .end(let trailers): | ||
let newTrailers = trailers.map { HTTPFields($0, splitCookie: false) } | ||
context.write(self.wrapOutboundOut(.end(newTrailers)), promise: promise) | ||
} | ||
} | ||
} |
Oops, something went wrong.