Skip to content
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

Use rawQueryString for APIGatewayV2 query values #23

Merged
merged 2 commits into from Mar 4, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 30 additions & 1 deletion Sources/HummingbirdLambda/APIGatewayLambda.swift
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
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
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
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()
}
}