-
Notifications
You must be signed in to change notification settings - Fork 491
Add ApiGatewayHttpApiV2ProxyRequestTranslator and ApiGatewayProxyRequestTranslator #1901
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
namespace Amazon.Lambda.TestTool.Extensions; | ||
|
||
using System.Text; | ||
using System.Web; | ||
using Amazon.Lambda.APIGatewayEvents; | ||
using Amazon.Lambda.TestTool.Models; | ||
using Amazon.Lambda.TestTool.Utilities; | ||
using static Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest; | ||
|
||
/// <summary> | ||
/// Provides extension methods to translate an <see cref="HttpContext"/> to different types of API Gateway requests. | ||
/// </summary> | ||
public static class HttpContextExtensions | ||
{ | ||
/// <summary> | ||
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayHttpApiV2ProxyRequest"/>. | ||
/// </summary> | ||
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param> | ||
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param> | ||
/// <returns>An <see cref="APIGatewayHttpApiV2ProxyRequest"/> object representing the translated request.</returns> | ||
public static async Task<APIGatewayHttpApiV2ProxyRequest> ToApiGatewayHttpV2Request( | ||
this HttpContext context, | ||
ApiGatewayRouteConfig apiGatewayRouteConfig) | ||
{ | ||
var request = context.Request; | ||
var currentTime = DateTimeOffset.UtcNow; | ||
var body = await HttpRequestUtility.ReadRequestBody(request); | ||
var contentLength = HttpRequestUtility.CalculateContentLength(request, body); | ||
|
||
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path); | ||
|
||
// Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field. | ||
// 2.0 also lowercases all header keys | ||
var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers, true); | ||
var headers = allHeaders.ToDictionary( | ||
kvp => kvp.Key, | ||
kvp => string.Join(", ", kvp.Value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: small inconsistency between headers and query params. Here you are joining on ", " and below on "," There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is intentional. from my testing the headers to have a space in them but query string params do not |
||
); | ||
|
||
// Duplicate query strings are combined with commas and included in the queryStringParameters field. | ||
var (_, allQueryParams) = HttpRequestUtility.ExtractQueryStringParameters(request.Query); | ||
var queryStringParameters = allQueryParams.ToDictionary( | ||
kvp => kvp.Key, | ||
kvp => string.Join(",", kvp.Value) | ||
); | ||
|
||
string userAgent = request.Headers.UserAgent.ToString(); | ||
|
||
if (!headers.ContainsKey("content-length")) | ||
{ | ||
headers["content-length"] = contentLength.ToString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we always update the content-length since we are updating the body? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so i checked this an apparently rest does not set this by default. Edit: that was for the v1 actually. i can still update this to always set it. let me double check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i checked on this. it needs to be the original content-length which is why i dont need to re-set it. my integration test fails (the real api gateway expects the original content length) |
||
} | ||
|
||
if (!headers.ContainsKey("content-type")) | ||
{ | ||
headers["content-type"] = "text/plain; charset=utf-8"; | ||
} | ||
|
||
var httpApiV2ProxyRequest = new APIGatewayHttpApiV2ProxyRequest | ||
{ | ||
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}", | ||
RawPath = request.Path.Value, // this should be decoded value | ||
Body = body, | ||
IsBase64Encoded = false, | ||
RequestContext = new ProxyRequestContext | ||
{ | ||
Http = new HttpDescription | ||
{ | ||
Method = request.Method, | ||
Path = request.Path.Value, // this should be decoded value | ||
Protocol = !string.IsNullOrEmpty(request.Protocol) ? request.Protocol : "HTTP/1.1", // defaults to http 1.1 if not provided | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we make the "HTTP/1.1" assumption? This is more likely to get updated and we wouldn't know to update it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ive put this here so it matches api gateway |
||
UserAgent = userAgent | ||
}, | ||
Time = currentTime.ToString("dd/MMM/yyyy:HH:mm:ss") + " +0000", | ||
TimeEpoch = currentTime.ToUnixTimeMilliseconds(), | ||
RequestId = HttpRequestUtility.GenerateRequestId(), | ||
RouteKey = $"{request.Method} {apiGatewayRouteConfig.Path}", | ||
}, | ||
Version = "2.0" | ||
}; | ||
|
||
if (request.Cookies.Any()) | ||
{ | ||
httpApiV2ProxyRequest.Cookies = request.Cookies.Select(c => $"{c.Key}={c.Value}").ToArray(); | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
if (headers.Any()) | ||
{ | ||
httpApiV2ProxyRequest.Headers = headers; | ||
} | ||
|
||
httpApiV2ProxyRequest.RawQueryString = string.Empty; // default is empty string | ||
|
||
if (queryStringParameters.Any()) | ||
{ | ||
// this should be decoded value | ||
httpApiV2ProxyRequest.QueryStringParameters = queryStringParameters; | ||
|
||
// this should be the url encoded value and not include the "?" | ||
// e.g. key=%2b%2b%2b | ||
httpApiV2ProxyRequest.RawQueryString = HttpUtility.UrlPathEncode(request.QueryString.Value?.Substring(1)); | ||
|
||
} | ||
|
||
if (pathParameters.Any()) | ||
{ | ||
// this should be decoded value | ||
httpApiV2ProxyRequest.PathParameters = pathParameters; | ||
} | ||
|
||
if (HttpRequestUtility.IsBinaryContent(request.ContentType)) | ||
{ | ||
// we already converted it when we read the body so we dont need to re-convert it | ||
httpApiV2ProxyRequest.IsBase64Encoded = true; | ||
} | ||
|
||
return httpApiV2ProxyRequest; | ||
} | ||
|
||
/// <summary> | ||
/// Translates an <see cref="HttpContext"/> to an <see cref="APIGatewayProxyRequest"/>. | ||
/// </summary> | ||
/// <param name="context">The <see cref="HttpContext"/> to be translated.</param> | ||
/// <param name="apiGatewayRouteConfig">The configuration of the API Gateway route, including the HTTP method, path, and other metadata.</param> | ||
/// <returns>An <see cref="APIGatewayProxyRequest"/> object representing the translated request.</returns> | ||
public static async Task<APIGatewayProxyRequest> ToApiGatewayRequest( | ||
this HttpContext context, | ||
ApiGatewayRouteConfig apiGatewayRouteConfig, | ||
ApiGatewayEmulatorMode emulatorMode) | ||
{ | ||
var request = context.Request; | ||
var body = await HttpRequestUtility.ReadRequestBody(request); | ||
var contentLength = HttpRequestUtility.CalculateContentLength(request, body); | ||
|
||
var pathParameters = RouteTemplateUtility.ExtractPathParameters(apiGatewayRouteConfig.Path, request.Path); | ||
|
||
var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers); | ||
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query); | ||
|
||
if (!headers.ContainsKey("content-length") && emulatorMode != ApiGatewayEmulatorMode.Rest) // rest doesnt set content-length by default | ||
{ | ||
headers["content-length"] = contentLength.ToString(); | ||
multiValueHeaders["content-length"] = [contentLength.ToString()]; | ||
} | ||
|
||
if (!headers.ContainsKey("content-type")) | ||
{ | ||
headers["content-type"] = "text/plain; charset=utf-8"; | ||
multiValueHeaders["content-type"] = ["text/plain; charset=utf-8"]; | ||
} | ||
|
||
// This is the decoded value | ||
var path = request.Path.Value; | ||
|
||
if (emulatorMode == ApiGatewayEmulatorMode.HttpV1 || emulatorMode == ApiGatewayEmulatorMode.Rest) // rest and httpv1 uses the encoded value for path an | ||
{ | ||
path = request.Path.ToUriComponent(); | ||
} | ||
|
||
if (emulatorMode == ApiGatewayEmulatorMode.Rest) // rest uses encoded value for the path params | ||
{ | ||
var encodedPathParameters = pathParameters.ToDictionary( | ||
kvp => kvp.Key, | ||
kvp => Uri.EscapeUriString(kvp.Value)); // intentionally using EscapeURiString over EscapeDataString since EscapeURiString correctly handles reserved characters :/?#[]@!$&'()*+,;= in this case | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for example. escapedatastring encodes * as %20 which is different than how api gateway rest api does it (it keeps * as *) |
||
pathParameters = encodedPathParameters; | ||
} | ||
|
||
var proxyRequest = new APIGatewayProxyRequest | ||
{ | ||
Resource = apiGatewayRouteConfig.Path, | ||
Path = path, | ||
HttpMethod = request.Method, | ||
Body = body, | ||
IsBase64Encoded = false | ||
}; | ||
|
||
if (headers.Any()) | ||
{ | ||
proxyRequest.Headers = headers; | ||
} | ||
|
||
if (multiValueHeaders.Any()) | ||
{ | ||
proxyRequest.MultiValueHeaders = multiValueHeaders; | ||
} | ||
|
||
if (queryStringParameters.Any()) | ||
{ | ||
// this should be decoded value | ||
proxyRequest.QueryStringParameters = queryStringParameters; | ||
} | ||
|
||
if (multiValueQueryStringParameters.Any()) | ||
{ | ||
// this should be decoded value | ||
proxyRequest.MultiValueQueryStringParameters = multiValueQueryStringParameters; | ||
} | ||
|
||
if (pathParameters.Any()) | ||
{ | ||
proxyRequest.PathParameters = pathParameters; | ||
} | ||
|
||
if (HttpRequestUtility.IsBinaryContent(request.ContentType)) | ||
{ | ||
proxyRequest.IsBase64Encoded = true; | ||
} | ||
|
||
return proxyRequest; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.