Skip to content

Commit

Permalink
Use rawQueryString for APIGatewayV2 query values (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
adam-fowler authored Mar 4, 2024
1 parent f454c62 commit e7ece11
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 33 deletions.
31 changes: 30 additions & 1 deletion Sources/HummingbirdLambda/APIGatewayLambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,36 @@ extension HBLambda where Output == APIGatewayResponse {
}

// conform `APIGatewayRequest` to `APIRequest` so we can use HBRequest.init(context:application:from)
extension APIGatewayRequest: APIRequest {}
extension APIGatewayRequest: APIRequest {
var queryString: String {
func urlPercentEncoded(_ string: String) -> String {
return string.addingPercentEncoding(withAllowedCharacters: .urlQueryComponentAllowed) ?? string
}
var queryParams: [String] = []
var queryStringParameters = self.queryStringParameters ?? [:]
// go through list of multi value query string params first, removing any
// from the single value list if they are found in the multi value list
self.multiValueQueryStringParameters?.forEach { multiValueQuery in
queryStringParameters[multiValueQuery.key] = nil
queryParams += multiValueQuery.value.map { "\(urlPercentEncoded(multiValueQuery.key))=\(urlPercentEncoded($0))" }
}
queryParams += queryStringParameters.map {
"\(urlPercentEncoded($0.key))=\(urlPercentEncoded($0.value))"
}
return queryParams.joined(separator: "&")
}

var httpHeaders: HTTPHeaders {
var headers = HTTPHeaders(self.headers.map { ($0.key, $0.value) })
self.multiValueHeaders.forEach { multiValueHeader in
headers.remove(name: multiValueHeader.key)
for header in multiValueHeader.value {
headers.add(name: multiValueHeader.key, value: header)
}
}
return headers
}
}

// conform `APIGatewayResponse` to `APIResponse` so we can use HBResponse.apiReponse()
extension APIGatewayResponse: APIResponse {}
Expand Down
6 changes: 4 additions & 2 deletions Sources/HummingbirdLambda/APIGatewayV2Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ extension APIGatewayV2Request: APIRequest {
}

var httpMethod: AWSLambdaEvents.HTTPMethod { context.http.method }
var multiValueQueryStringParameters: [String: [String]]? { nil }
var multiValueHeaders: HTTPMultiValueHeaders { [:] }
var queryString: String { self.rawQueryString }
var httpHeaders: HTTPHeaders {
return HTTPHeaders(self.headers.map { ($0.key, $0.value) })
}
}

// conform `APIGatewayV2Response` to `APIResponse` so we can use HBResponse.apiReponse()
Expand Down
29 changes: 5 additions & 24 deletions Sources/HummingbirdLambda/Request+APIGateway.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ import NIOHTTP1
protocol APIRequest {
var path: String { get }
var httpMethod: AWSLambdaEvents.HTTPMethod { get }
var queryStringParameters: [String: String]? { get }
var multiValueQueryStringParameters: [String: [String]]? { get }
var headers: AWSLambdaEvents.HTTPHeaders { get }
var multiValueHeaders: HTTPMultiValueHeaders { get }
var queryString: String { get }
var httpHeaders: HTTPHeaders { get }
var body: String? { get }
var isBase64Encoded: Bool { get }
}
Expand All @@ -42,28 +40,11 @@ extension HBRequest {
}
// construct URI with query parameters
var uri = from.path
var queryParams: [String] = []
var queryStringParameters = from.queryStringParameters ?? [:]
// go through list of multi value query string params first, removing any
// from the single value list if they are found in the multi value list
from.multiValueQueryStringParameters?.forEach { multiValueQuery in
queryStringParameters[multiValueQuery.key] = nil
queryParams += multiValueQuery.value.map { "\(urlPercentEncoded(multiValueQuery.key))=\(urlPercentEncoded($0))" }
}
queryParams += queryStringParameters.map {
"\(urlPercentEncoded($0.key))=\(urlPercentEncoded($0.value))"
}
if queryParams.count > 0 {
uri += "?\(queryParams.joined(separator: "&"))"
if from.queryString.count > 0 {
uri += "?\(from.queryString)"
}
// construct headers
var headers = NIOHTTP1.HTTPHeaders(from.headers.map { ($0.key, $0.value) })
from.multiValueHeaders.forEach { multiValueHeader in
headers.remove(name: multiValueHeader.key)
for header in multiValueHeader.value {
headers.add(name: multiValueHeader.key, value: header)
}
}
let headers = from.httpHeaders
let head = HTTPRequestHead(
version: .init(major: 2, minor: 0),
method: .init(rawValue: from.httpMethod.rawValue),
Expand Down
83 changes: 77 additions & 6 deletions Tests/HummingbirdLambdaTests/LambdaTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,23 @@ final class LambdaTests: XCTestCase {
)
}

func newEvent(uri: String, method: String, body: ByteBuffer? = nil) throws -> APIGatewayRequest {
func newEvent(
uri: String,
method: String,
queryValues: [String: String]? = nil,
multiQueryValues: [String: [String]]? = nil,
body: ByteBuffer? = nil
) throws -> APIGatewayRequest {
let base64Body = body.map { "\"\(String(base64Encoding: $0.readableBytesView))\"" } ?? "null"
let queryValuesString = try queryValues.map { try String(decoding: JSONEncoder().encode($0), as: UTF8.self) } ?? "null"
let multiQueryValuesString = try multiQueryValues.map { try String(decoding: JSONEncoder().encode($0), as: UTF8.self) } ?? "null"
let request = """
{"httpMethod": "\(method)", "body": \(base64Body), "resource": "\(uri)", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "\(uri)", "httpMethod": "\(method)", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "\(uri)"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "\(uri)", "isBase64Encoded": \(body != nil)}
{"httpMethod": "\(method)", "body": \(base64Body), "resource": "\(uri)", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "\(uri)", "httpMethod": "\(method)", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "Prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "\(uri)"}, "queryStringParameters": \(queryValuesString), "multiValueQueryStringParameters": \(multiQueryValuesString), "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Cache-Control": "max-age=0", "Dnt": "1", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24", "Sec-Fetch-User": "?1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Cache-Control": ["max-age=0"], "Dnt": ["1"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36 Edg/78.0.276.24"], "Sec-Fetch-User": ["?1"], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"], "Sec-Fetch-Site": ["none"], "Sec-Fetch-Mode": ["navigate"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "\(uri)", "isBase64Encoded": \(body != nil)}
"""
return try JSONDecoder().decode(APIGatewayRequest.self, from: Data(request.utf8))
}

func newV2Event(uri: String, method: String) throws -> APIGatewayV2Request {
func newV2Event(uri: String, method: String, rawQueryString: String = "") throws -> APIGatewayV2Request {
let request = """
{
"routeKey":"\(method) \(uri)",
Expand Down Expand Up @@ -103,7 +111,7 @@ final class LambdaTests: XCTestCase {
"time":"24/Apr/2020:17:47:41 +0000"
},
"isBase64Encoded":false,
"rawQueryString":"foo=bar",
"rawQueryString":"\(rawQueryString)",
"queryStringParameters":{
"foo":"bar"
},
Expand Down Expand Up @@ -174,14 +182,17 @@ final class LambdaTests: XCTestCase {

init(_ app: HBApplication) {
app.middleware.add(HBLogRequestsMiddleware(.debug))
app.router.post { _ in
app.router.post("test") { request in
XCTAssertEqual(request.method, .POST)
XCTAssertEqual(request.uri.path, "/test")
XCTAssertNil(request.uri.query)
return "hello"
}
}
}
let lambda = try HBLambdaHandler<HelloLambda>.makeHandler(context: self.initializationContext).wait()
let context = self.newContext()
let event = try newV2Event(uri: "/", method: "POST")
let event = try newV2Event(uri: "/test", method: "POST")
let response = try lambda.handle(event, context: context).wait()
XCTAssertEqual(response.statusCode, .ok)
XCTAssertEqual(response.body, "hello")
Expand All @@ -207,4 +218,64 @@ final class LambdaTests: XCTestCase {
XCTAssertEqual(response.statusCode, .badRequest)
XCTAssertEqual(response.body, "BadRequest")
}

func testAPIGatewayQueryValues() throws {
struct HelloLambda: HBLambda {
// define input and output
typealias Event = APIGatewayRequest
typealias Output = APIGatewayResponse

init(_ app: HBApplication) {
app.middleware.add(HBLogRequestsMiddleware(.debug))
app.router.post { request -> HTTPResponseStatus in
XCTAssertEqual(request.uri.queryParameters["foo"], "bar")
return .ok
}
}
}
let lambda = try HBLambdaHandler<HelloLambda>.makeHandler(context: self.initializationContext).wait()
let context = self.newContext()
let event = try newEvent(uri: "/", method: "POST", queryValues: ["foo": "bar"])
_ = try lambda.handle(event, context: context).wait()
}

func testAPIGatewayMultiQueryValues() throws {
struct HelloLambda: HBLambda {
// define input and output
typealias Event = APIGatewayRequest
typealias Output = APIGatewayResponse

init(_ app: HBApplication) {
app.middleware.add(HBLogRequestsMiddleware(.debug))
app.router.post { request -> HTTPResponseStatus in
XCTAssertEqual(request.uri.queryParameters.getAll("foo"), ["bar1", "bar2"])
return .ok
}
}
}
let lambda = try HBLambdaHandler<HelloLambda>.makeHandler(context: self.initializationContext).wait()
let context = self.newContext()
let event = try newEvent(uri: "/", method: "POST", queryValues: ["foo": "bar"], multiQueryValues: ["foo": ["bar1", "bar2"]])
_ = try lambda.handle(event, context: context).wait()
}

func testAPIGateway2QueryValues() throws {
struct HelloLambda: HBLambda {
// define input and output
typealias Event = APIGatewayV2Request
typealias Output = APIGatewayV2Response

init(_ app: HBApplication) {
app.middleware.add(HBLogRequestsMiddleware(.debug))
app.router.post { request -> HTTPResponseStatus in
XCTAssertEqual(request.uri.queryParameters["foo"], "bar")
return .ok
}
}
}
let lambda = try HBLambdaHandler<HelloLambda>.makeHandler(context: self.initializationContext).wait()
let context = self.newContext()
let event = try newV2Event(uri: "/", method: "POST", rawQueryString: "foo=bar")
_ = try lambda.handle(event, context: context).wait()
}
}

0 comments on commit e7ece11

Please sign in to comment.