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

Add ApiGatewayHttpApiV2ProxyRequestTranslator and ApiGatewayProxyRequestTranslator #1901

Merged
merged 3 commits into from
Dec 23, 2024

Conversation

gcbeattyAWS
Copy link

@gcbeattyAWS gcbeattyAWS commented Dec 9, 2024

DOTNET-7838

Description of changes:

  1. Add functions to translate httpcontext into APIGatewayHttpApiV2ProxyRequest and APIGatewayProxyRequest
  2. Add unit tests for above classes

Testing details

  1. Wrote unit tests
  2. integration tests

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@gcbeattyAWS gcbeattyAWS requested a review from philasmar December 9, 2024 15:17
@gcbeattyAWS gcbeattyAWS marked this pull request as ready for review December 9, 2024 15:17
@gcbeattyAWS gcbeattyAWS requested a review from normj December 9, 2024 15:45
@gcbeattyAWS gcbeattyAWS added the Release Not Needed Add this label if a PR does not need to be released. label Dec 10, 2024
@gcbeattyAWS gcbeattyAWS marked this pull request as draft December 10, 2024 19:23
@gcbeattyAWS gcbeattyAWS changed the base branch from feature/lambdatesttool-v2 to asmarp/api-gateway-emulator-skeleton December 10, 2024 19:31
@gcbeattyAWS gcbeattyAWS marked this pull request as ready for review December 10, 2024 19:37
Copy link
Member

@normj normj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any tests for special characters in headers, resource paths or query strings. Have we done a comparison yet with API Gateway? We should have tests for those cases. That includes the RawPath property as well.

@gcbeattyAWS
Copy link
Author

gcbeattyAWS commented Dec 11, 2024

I don't see any tests for special characters in headers, resource paths or query strings. Have we done a comparison yet with API Gateway? We should have tests for those cases. That includes the RawPath property as well.

so i did some tests of checking the existing functionality of api gateway in production, with special characters in the header by

  1. deploying an api gateway
  2. deploying a lambda
  3. attaching the lambda to the api gatway
  4. invoking the api gateway via command line (curl), and sending a non url encoded header value of special+++
  5. logging the event (apigatewayrequest) object in the lambda itself, and it prints out special+++ (not encoded), so i assumed no encoding is required for that - but let me double check on this and get back to you

if (string.IsNullOrEmpty(contentType))
return false;

return contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this list of content types we use for response in the ASP.NET Core bridge library. https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.AspNetCoreServer/AbstractAspNetCoreFunction.cs#L59

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also think about how this can be configurable. Probably via environment variables that we can have users configure in Aspire.

Headers = headers,
QueryStringParameters = queryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Body = HttpRequestUtility.ReadRequestBody(request),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you take note that when we get these ToAPIGatewayXXX() methods hooked up to the main code path that is asking for the APIGatewayHttpApiV2ProxyRequest. When we convert the APIGatewayHttpApiV2ProxyRequest to the MemoryStream to Lambda we should check the size of the stream is no more then 6MB. If so throw back the same error that users would get from API Gateway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i will connect with phil on where he wants to do this logic at

MultiValueHeaders = multiValueHeaders,
QueryStringParameters = queryStringParameters,
MultiValueQueryStringParameters = multiValueQueryStringParameters,
PathParameters = pathParameters ?? new Dictionary<string, string>(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we initializing this to an empty collection if we don't have any. Is this what API Gateway does?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. i just did a test
Invoke-WebRequest -Uri https://myapi-gateway-url.amazonaws.com/testfunction -Method GET
where it echos the api gateway request object and i see the value is null when there are no path parameters.

pathParameters: null,
  stageVariables: null,
  body: null,
  isBase64Encoded: false
}

i will update the code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ill also double check the query string and header default values

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ive updated the logic to handle this now


foreach (var header in headers)
{
singleValueHeaders[header.Key] = header.Value.Last() ?? "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My memory for API Gateway is that in HTTP V2 where there is just a single headers collection the headers are comma delimited. But we need to verify that. I'm sure it isn't just get the last value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah you are right on this https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html

Format 2.0 doesn't have multiValueHeaders or multiValueQueryStringParameters fields. Duplicate headers are combined with commas and included in the headers field. Duplicate query strings are combined with commas and included in the queryStringParameters field.

i think i got confused when reading the other docs for v1 https://aws.amazon.com/blogs/compute/support-for-multi-value-parameters-in-amazon-api-gateway/

Before this change, API Gateway used to retain only the last value and drop everything else for a multi-valued parameter. You can see the original behavior in the queryStringParameters parameter in the above input, where only the “fish” value is retained.

I will update accordingly

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ive updated the logic to handle this now. this logic will still just return the dicts as-is but then i have the caller perform the comma separated merging.


foreach (var param in query)
{
singleValueParams[param.Key] = param.Value.Last() ?? "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above for ExtractHeaders

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ive updated the logic to handle this now

@normj
Copy link
Member

normj commented Dec 11, 2024

4. invoking the api gateway via command line (curl), and sending a non url encoded header value of special+++

@gcbeattyAWS Also check more characters then + like spaces and other white space and Unicode characters. resource path and query strings can also be special. Especially when you through /, & and `?' in the values for resource path segments and query string values. In the ASP.NET Core Lambda bridge library where I have to do all of this in reverse I had to do some encoding handling.

internal static string CreateQueryStringParameters(IDictionary<string, string> singleValues, IDictionary<string, IList<string>> multiValues, bool urlEncodeValue)

@gcbeattyAWS
Copy link
Author

gcbeattyAWS commented Dec 12, 2024

  1. invoking the api gateway via command line (curl), and sending a non url encoded header value of special+++

@gcbeattyAWS Also check more characters then + like spaces and other white space and Unicode characters. resource path and query strings can also be special. Especially when you through /, & and `?' in the values for resource path segments and query string values. In the ASP.NET Core Lambda bridge library where I have to do all of this in reverse I had to do some encoding handling.

internal static string CreateQueryStringParameters(IDictionary<string, string> singleValues, IDictionary<string, IList<string>> multiValues, bool urlEncodeValue)

so i have updated the logic based on my findings:1

  1. For query strings. QueryStringParameters should be the decoded values (this already happens automatically when accessing Request.QueryString. RawQueryString should be the encoded query string without the ?.
  2. Headers do not seem to have any additional logic of decoding that i have seen. they are just a pass-through to lambda.
  3. The path and path parameters should be decoded (this is also happening automatically now by accessing via request.Path)
  4. The api gatway cannot handle %2f in the urls ("/") character

i have added test cases to check all of these scenarios

@gcbeattyAWS gcbeattyAWS requested a review from normj December 12, 2024 17:15
@gcbeattyAWS gcbeattyAWS marked this pull request as draft December 18, 2024 00:19
@gcbeattyAWS gcbeattyAWS changed the base branch from asmarp/api-gateway-emulator-skeleton to gcbeatty/responseparsing December 19, 2024 16:09
@gcbeattyAWS
Copy link
Author

pushing what i have so far

var (_, allHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var headers = allHeaders.ToDictionary(
kvp => kvp.Key,
kvp => string.Join(", ", kvp.Value)
Copy link
Contributor

Choose a reason for hiding this comment

The 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 ","

Copy link
Author

Choose a reason for hiding this comment

The 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


if (!headers.ContainsKey("content-length"))
{
headers["content-length"] = contentLength.ToString();
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 19, 2024

Choose a reason for hiding this comment

The 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

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 20, 2024

Choose a reason for hiding this comment

The 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)

{
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ive put this here so it matches api gateway

var (headers, multiValueHeaders) = HttpRequestUtility.ExtractHeaders(request.Headers);
var (queryStringParameters, multiValueQueryStringParameters) = HttpRequestUtility.ExtractQueryStringParameters(request.Query);

if (!headers.ContainsKey("content-length"))
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea thats a good point. it doesnt hurt to always update it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like rest api doesnt set this automatically by default

/// X-Custom-Header: value1
///
/// The method will return:
/// singleValueHeaders: { "Accept": "application/xhtml+xml", "X-Custom-Header": "value1" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we taking the last occurrence of Accept? Is that a valid assumption?

Copy link
Author

@gcbeattyAWS gcbeattyAWS Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So when api gateway receives a request with headers

Accept: a
Accept: b

the single value header would be Accept: b

However, in .net headers are always sent to api gateway like

Accept: a, b

and singleValue header would be Accept: a, b.

so depending what we want to do we can either do the intended way by api gateway, or do `, '.join(header.value) for the singelvalue header and mimic what the value would be if coming from .net.

This same issue applies for multivalueheaders

when api gateway receives a request with headers

Accept: a
Accept: b

multi value headers would be `accept: ['a', 'b']

but when sending from .net as

Accept: a, b

multivalueheadeders would be `accept: ['a, b'] (the string)

/// For query string: ?param1=value1&amp;param2=value2&amp;param2=value3
///
/// The method will return:
/// singleValueParams: { "param1": "value1", "param2": "value3" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here

/// The generated ID is a 145character string consisting of lowercase letters and numbers, followed by an equals sign.
public static string GenerateRequestId()
{
return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return Guid.NewGuid().ToString("N").Substring(0, 8) + Guid.NewGuid().ToString("N").Substring(0, 7) + "=";
return $"{Guid.NewGuid().ToString("N").Substring(0, 8)} {Guid.NewGuid().ToString("N").Substring(0, 7)}=";

/// // parameters will contain: { {"id", "123"}, {"orderId", "456"} }
/// </code>
/// </example>
public static Dictionary<string, string> ExtractPathParameters(string routeTemplate, string actualPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not take into account the greedy variable {proxy+}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

/// // defaults will contain: { {"version", "v1"} }
/// </code>
/// </example>
public static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed

@gcbeattyAWS gcbeattyAWS changed the base branch from gcbeatty/responseparsing to feature/lambdatesttool-v2 December 20, 2024 20:17
@gcbeattyAWS gcbeattyAWS marked this pull request as ready for review December 20, 2024 20:17
}

[Fact]
public async Task BinaryContentHttpV1()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still need an integration test for the rest api for binary content. it was taking too long to setup though because i think need to create a new lambda, api gateway with binary types configured and all that. i can create a backlog item to add this in next pr

{
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
Copy link
Author

Choose a reason for hiding this comment

The 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 *)

[Theory]
[InlineData(ApiGatewayEmulatorMode.Rest)]
[InlineData(ApiGatewayEmulatorMode.HttpV1)]
public async Task ToApiGateway_MultiValueHeader(ApiGatewayEmulatorMode emulatorMode)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i manually tested these unit tests

@@ -16,17 +16,39 @@ public class ApiGatewayIntegrationTestFixture : IAsyncLifetime
public ApiGatewayTestHelper ApiGatewayTestHelper { get; private set; }

public string StackName { get; private set; }
public string RestApiId { get; private set; }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just renaming

@gcbeattyAWS gcbeattyAWS merged commit 68ff1db into feature/lambdatesttool-v2 Dec 23, 2024
4 checks passed
@gcbeattyAWS gcbeattyAWS deleted the gcbeatty/apiparse branch December 23, 2024 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release Not Needed Add this label if a PR does not need to be released.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants