Building a Proxy Server with Hummingbird
Hummingbird is a powerful and flexible system for creating various web applications. Its design enables running on a tiny memory footprint, allowing it to efficiently handle a lot of throughput.
The asynchronous and flexible nature of Hummingbird allows you to create HTTP proxies extremely easily.
Routing Requests
To create a proxy server, you can use three ways to intercept the incoming request:
The first way is to create route on the Router
, for example, using a (recursive) wildcard. The route associated with these paths will receive this request. It can can then modify the target server and forward the request there.
The second solution is to create a RouterMiddleware
that intercepts the request. This can be applied to a route group or globally to all requests.
Finally, you can forward every request by intercepting them all in a custom HTTPResponder
.
Each of these solutions will need to import the necessary modules:
import AsyncHTTPClient
import Hummingbird
import Logging
import NIOCore
import NIOPosix
import ServiceLifecycle
hummingbird-2-proxy.swiftForwarding Requests
Each of the above solutions forwards requests the same way. The actual forwarding logic is only invoked differently for each solution.
To forward a request, you need to create a new HTTPClientRequest
as defined by the AsyncHTTPClient
library. Then, swap the target host (your API) with a new host. You can then use the HTTPClient
to send the request and receive the response.
The HTTP Client is swappable in this function, but the rest of this tutorial will use the HTTPClient.shared
.
func forward(
request: Request,
targetHost: String,
httpClient: HTTPClient,
context: some RequestContext
) async throws -> Response {
// 1.
let query = request.uri.query.map { "?\($0)" } ?? ""
var clientRequest = HTTPClientRequest(url: "https://\(targetHost)\(request.uri.path)\(query)")
clientRequest.method = .init(request.method)
clientRequest.headers = .init(request.headers)
// 2.
let contentLength = if let header = request.headers[.contentLength], let value = Int(header) {
HTTPClientRequest.Body.Length.known(value)
} else {
HTTPClientRequest.Body.Length.unknown
}
clientRequest.body = .stream(
request.body,
length: contentLength
)
// 3.
let response = try await httpClient.execute(clientRequest, timeout: .seconds(60))
// 4.
return Response(
status: HTTPResponse.Status(code: Int(response.status.code), reasonPhrase: response.status.reasonPhrase),
headers: HTTPFields(response.headers, splitCookie: false),
body: ResponseBody(asyncSequence: response.body)
)
}
hummingbird-2-proxy.swift:10As you see, there are four steps to forward a request:
Modify the incoming
Request
’s URL, Method and Headers to point to the target server.Provide the request’s
RequestBody
to theHTTPClient
in a streaming fashion.Execute the request using
HTTPClient
.Forward the
Response
back to the end user.
In both step 2 and step 4, the HTTP body is passed along as a stream, or specifically an AsyncSequence<ByteBuffer>
. This allows SwiftNIO to pass along the data efficiently, whilst applying backpressure in both directions (client and remote).
Due to the design of Hummingbird, where bodies are always streamed, this is not just an efficient solution but also the easiest to implement.
Custom Route
The first option is to create a route on the Router
that will intercept the request and forward it to the target server.
Since the code is the same as the previous example, we will only need to invoke the forward
function from within this middleware.
let router = Router()
router.get("**") { request, context in
try await forward(
request: request,
targetHost: "example.com",
httpClient: .shared,
context: context
)
}
hummingbird-2-proxy.swift:78Middleware
For the second option you need to create a new RouterMiddleware
. This middleware will intercept the request and forward it to the target server.
struct ProxyServerMiddleware<Context: RequestContext>: RouterMiddleware {
var httpClient: HTTPClient = .shared
let targetHost: String
func handle(_ request: Request, context: Context, next: (Request, Context) async throws -> Response) async throws -> Response {
try await forward(
request: request,
targetHost: targetHost,
httpClient: httpClient,
context: context
)
}
}
hummingbird-2-proxy.swift:46Then, once the middleware is set up, you can Router.add(middleware:)
to the router.
let router = Router()
router.add(middleware: ProxyServerMiddleware(targetHost: "example.com"))
hummingbird-2-proxy.swift:63Custom Responder
The final option will implement an HTTPResponder
.
struct ProxyServerResponder<Context: RequestContext>: HTTPResponder {
let targetHost: String
func respond(to request: Request, context: Context) async throws -> Response {
try await forward(
request: request,
targetHost: targetHost,
httpClient: .shared,
context: context
)
}
}
hummingbird-2-proxy.swift:93Then, you can set up the responder in the Application
.
let app = Application(
responder: ProxyServerResponder<BasicRequestContext>(targetHost: "example.com")
)
hummingbird-2-proxy.swift:109Conclusion
Hummingbird’s powerful design makes it easy to stream data in- and out of your applications. This makes it a great choice for creating a proxy server. Each of the three solutions presented in this tutorial can be used to create a proxy server with Hummingbird. Choose the one that best fits your needs and enjoy the power of Hummingbird.
Related posts
Using Hummingbird's Request Contexts
Learn about request contexts in Hummingbird and how to use them.
Getting Started with Hummingbird
Learn how to get started with Hummingbird 2, the modern Swift Web Framework.
Working with UDP in SwiftNIO
Create UDP servers and clients using SwiftNIO and structured concurrency
Using WebSockets in Hummingbird
In this article, you will learn about WebSockets and how to use them with the Hummingbird framework in a straightforward, easy-to-follow manner.
Using OpenAPI Generator with Hummingbird
Learn how to use OpenAPI Generator to create Swift APIs with Hummingbird.
What's new in Hummingbird 2?
Discover Hummingbird 2: a Swift-based HTTP server framework, with modern concurrency and customizable request contexts.