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

Local support for response multiValueHeaders #1166

Merged
merged 5 commits into from May 23, 2019

Conversation

dsanders11
Copy link
Contributor

Issue #, if available:

#830

Description of changes:

This is a rework of #842 by @martysweet, which seems to have stalled. This PR uses Flask's built-in method of providing multiple header values, which makes it act like API GW. In particular, multi-value headers are folded into a comma-separated list unless it's Set-Cookie which is a special-case.

There's two parts to this PR. First drops CaseInsensitiveDict as it doesn't seem to be necessary. The Headers class in Flask is already case-insensitive and swapping in Headers in the test case for CaseInsensitiveDict passed the tests. There are some minor differences:

  • Headers isn't JSON serializable by default
  • Headers won't pass equality comparison with a dict
  • Headers raises werkzeug.exceptions.BadRequestKeyError (a subclass of KeyError) on bad key

Those could be addressed with a custom subclass of Headers if necessary, but at the moment all tests pass just fine.

The second commit is mostly the same as #842 but uses the default behavior of Headers to handle multi-value headers instead of manually merging them as a comma-separated list. This lets Flask do the special-case for Set-Cookie.

@martysweet, @jfuss, @everyonce, you guys want to take a look since you were active on #842?

Checklist:

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

@jfuss
Copy link
Contributor

jfuss commented May 8, 2019

@dsanders11 I believe CaseInsensitiveDict is required. It was initially brought up here: #398. Nothing should have changed to not make this true anymore, unless I am miss understanding the problem or solution.

@dsanders11
Copy link
Contributor Author

dsanders11 commented May 8, 2019

Nothing should have changed to not make this true anymore, unless I am miss understanding the problem or solution.

@jfuss, I think you may have missed that CaseSensitiveDict was replaced with Headers, which also acts in a case-insensitive way. Prior to that PR the headers were being passed around as a simple dict. Basically Headers is a class from Flask which does the same thing as CaseSensitiveDict with a few extra bells and whistles (handles multi-value headers), and a few minor differences in functionality which I mentioned in the opening comment. When providing headers to a Flask Response it will turn the dict into a Headers object if it's not already.

That PR also added a functional test for the case-insensitivity, which this PR continues to pass.

@jfuss
Copy link
Contributor

jfuss commented May 8, 2019

I think you may have missed that CaseSensitiveDict was replaced with Headers, which also acts in a case-insensitive way.

@dsanders11 Yes, yes I did. I think that generally makes sense. Sorry for the oversight.

jfuss
jfuss previously requested changes May 13, 2019
Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

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

Overall this looks pretty good. My main request is to add additional integ tests that cover this multiple header case(s). A good place to put them would be under this test class: https://github.com/awslabs/aws-sam-cli/blob/develop/tests/integration/local/start_api/test_start_api.py#L291 We will need to add another function under the testdata directory so that it returns the multiple headers back from the function.

@@ -271,11 +274,44 @@ def _should_base64_decode_body(binary_types, flask_request, lamba_response_heade
True if the body from the request should be converted to binary, otherwise false

"""
best_match_mimetype = flask_request.accept_mimetypes.best_match([lamba_response_headers["Content-Type"]])
best_match_mimetype = flask_request.accept_mimetypes.best_match(lamba_response_headers.get_all("Content-Type"))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you know if this is the right behavior that matches API Gateway? Seems reasonable just want to make sure we emulate the service as closely as possible.

@dsanders11
Copy link
Contributor Author

I assume by the testdata directory you mean https://github.com/awslabs/aws-sam-cli/tree/develop/tests/integration/testdata/start_api? Taking a quick look, I see there's a swagger-template.yaml and a template.yaml. They look similar, but the swagger-template.yaml hasn't been touched in a year, and the test you linked refers to just template.yaml. Do you know if swagger-template.yaml is in use?

@jfuss
Copy link
Contributor

jfuss commented May 14, 2019

@dsanders11

I assume by the testdata directory you mean https://github.com/awslabs/aws-sam-cli/tree/develop/tests/integration/testdata/start_api?

Correct. That is the test data for that Test Class I linked.

The swagger-template.yaml is used in these Test Cases. The main difference between the swagger-template.yaml and template.yaml is how they define the API to test. The swagger uses inline swagger while the template uses the Event Api structure. Since what you are adding is based on the output of the function and not how it is defined, which one you use doesn't really matter all that much. The Test Class I pointed to seemed like a good fit and is already bootstrapped with a template file. It is much easier to define the template.yaml than the swagger-template.yaml.

@dsanders11
Copy link
Contributor Author

@jfuss, thanks for the info. I'll try to get a test written there in the next couple days, and confirm the behavior of Content-Type on API Gateway.

@Nevett
Copy link

Nevett commented May 15, 2019

This is an even bigger issue now, since v0.16.0 adds overly strict checks on the response, and errors with any that contain multiValueHeaders. This includes any responses that come from Amazon's own AspNetCoreServer.

See: https://github.com/awslabs/aws-sam-cli/blob/43a7c37/samcli/local/apigw/local_apigw_service.py#L242

@johnjelinek
Copy link

johnjelinek commented May 15, 2019

@dsanders11: how's this looking? I'm blocked on testing golang lambda handlers until this is merged.

EDIT: I was able to revert to 0.15.0 with brew like this:

cd $(brew --repo aws/tap)
git checkout 1dbd8dbdb86573d16de3a793106039c665f45949
HOMEBREW_NO_AUTO_UPDATE=1 brew reinstall aws/tap/aws-sam-cli

(this should've been easier, but now I'm unblocked 😄)

@jfuss jfuss force-pushed the response-multivalueheaders branch from fec27f9 to d53225c Compare May 15, 2019 22:46
@jfuss
Copy link
Contributor

jfuss commented May 15, 2019

@dsanders11 I added integ tests and rebased on top of develop (required a force push sorry). Not fair to put you in the hot path due to something we just released.

@sanathkr, @awood45, or @thesriram Can you take over reviewing as a different set of eyes.

@dsanders11
Copy link
Contributor Author

@jfuss, I was actually writing the integration test today (no big deal), but got sidetracked investigating the Content-Type behavior in API Gateway.

That led to me noticing that API Gateway doesn't seem to be behaving like the documentation says it should (and how this PR is written). Emphasis on the important part:

If you specify values for both headers and multiValueHeaders, API Gateway merges them into a single list. If the same key-value pair is specified in both, only the values from multiValueHeaders will appear in the merged list.

From my testing, here's what I'm seeing in the AWS Console when testing:

Thu May 16 00:02:36 UTC 2019 : Endpoint response body before transformations: {"body":"aGVsbG8=","statusCode":200,"headers":{"X-Foo":"bar","Content-Type":"text/html"},"multiValueHeaders":{"Content-Type":["application/json"],"X-Foo":["foo"]},"isBase64Encoded":true}

Thu May 16 00:02:36 UTC 2019 : Method response headers: {Content-Type=application/json,text/html, X-Foo=foo,bar, X-Amzn-Trace-Id=Root=1-5cdca899-c034cb57145c72f94b4d4592;Sampled=0}

Am I misreading the documentation, or is that not behaving like it says it should behave? It's merging the values from headers and multiValueHeaders into a combined list for the same header.

P.S. Regarding the breakage for some people, seems like you should be able to grab the very isolated change of allowing multiValueHeaders in the lambda response until this PR is done.

@jfuss
Copy link
Contributor

jfuss commented May 16, 2019

@dsanders11 My understanding from the docs is: the final header list is the set of headers and multiValueHeaders. I think what it’s trying to say is if multiValueHeaders also has text/html in the list (in your example), text/html would only appear once is the final headers that apigateway returns.

@dsanders11
Copy link
Contributor Author

@jfuss, that does seem to be the actual behavior on testing. Unfortunate that the documentation is ambiguous as both @martysweet and I interpreted it differently. I think it's the plural nature of 'only the values from multiValueHeaders will appear in the merged list.'

I've updated the code to act more like API Gateway does in actuality. This includes mimicking the order of header values when the same header appears in both header and multiHeaderValues, since this i important. From testing it appends the header value onto the end of the multiHeaderValues list.

The integration test will need to be updated to match this change.

Unfortunately I'm out of time to work on this anymore this week. I spent an hour trying to get base64 decoding working on API Gateway so I could confirm behavior, but was not able to do so despite seemingly following everything to a T. As such that part of this PR is still a question mark on if it exactly follows API Gateway.

@jfuss
Copy link
Contributor

jfuss commented May 17, 2019

@dsanders11 We really appreciate you (and @martysweet) diving into and ensuring this accurately reflects Api Gateway's behavior. I can take over the last bit here.

@AyushG3112
Copy link

Is this the root cause of #1190? If yes, can this be merged and released soon as this is causing issues during our development.

@jfuss jfuss mentioned this pull request Jul 2, 2019
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants