-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
add support for xml to json responses #9102
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 support for xml to json responses #9102
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for adding this!
I’m not sure what was the behaviour of AWS before SQS was able to return JSON, and if this is a result of it, or if they also convert the XML under the hood. See #8267 (comment) and #8268
Basically, I’m wondering if AWS uses JSON under the hood for the SQS response instead of parsing XML, or if their implementation of VTL is able to take XML as input. I suppose could test it with an HTTP integration and trying to do the same, and see if that works.
I think it doesn’t hurt to be able to do it now, but I’m not sure if this should be only for SQS until we implement JSON, or if it should be part of every template rendering logic.
After giving it a try in the AWS console, it seems AWS is unable to do so, it can’t parse XML as input from the integration response, I tried with http://httpbin.org/xml and the template was not being applied nor parsed.
This was the API I tried it on with the nice test console.
openapi: "3.0.1"
info:
title: "test-api-xml"
version: "2023-09-09T15:26:38Z"
servers:
- url: "/{basePath}"
variables:
basePath:
default: "test"
paths:
/test:
get:
responses:
"200":
description: "200 response"
content:
application/json:
schema:
$ref: "#/components/schemas/Empty"
x-amazon-apigateway-integration:
httpMethod: "GET"
uri: "http://httpbin.org/xml"
responses:
default:
statusCode: "200"
responseTemplates:
application/json: "#set($responseBody = $input.path('$.slideshow'))\n\
#set($requestId = $input.path('$.slideshow.slide[0].title'))\n#set($messageId\
\ = $slideshow.slide[1].title)\n{\n \"title1\": \"$requestId\"\
,\n \"title2\": \"$messageId\"\n}\n"
passthroughBehavior: "when_no_match"
type: "http"
components:
schemas:
Empty:
title: "Empty Schema"
type: "object"
The logs from the test console seemed to show it wasn't parsing the response, but if you'd like to give it a try too, feel free as I'm not 100% sure if I missed a step. I tried to look online and it seems people went to great length to be able to just only pass XML from a lambda.
I also tried an SQS API, and you can see API Gateway is sending the Accept=application/json
header, so SQS should respond in JSON too.
Logs from the test console:
Sat Sep 09 15:57:46 UTC 2023 : Sending request to https://sqs.us-east-1.amazonaws.com/<account-id>/queue-518a623e
Sat Sep 09 15:57:46 UTC 2023 : Received response. Status: 200, Integration latency: 34 ms
Sat Sep 09 15:57:46 UTC 2023 : Endpoint response headers: {x-amzn-RequestId=315726e7-2463-5735-80bc-7b91bc59149e, X-Amzn-Trace-Id=Root=1-64fc95fa-630fac6d04ebca8e4d4f74ae, Date=Sat, 09 Sep 2023 15:57:46 GMT, Content-Type=application/json, Content-Length=312, connection=keep-alive}
Sat Sep 09 15:57:46 UTC 2023 : Endpoint response body before transformations: {"SendMessageResponse":{"ResponseMetadata":{"RequestId":"315726e7-2463-5735-80bc-7b91bc59149e"},"SendMessageResult":{"MD5OfMessageAttributes":null,"MD5OfMessageBody":"0c38abe3676e5cf7048b3e40018ea418","MD5OfMessageSystemAttributes":null,"MessageId":"a8e25ba3-2165-45d0-8e71-db475f6a4714","SequenceNumber":null}}}
So I’d be in favour to not have this as part of the global template rendering, but just in the SQS integration for now, until we can force the Content-Type
to application/json
to SQS so that it can return JSON natively so we could remove this workaround. What do you think? I don't know if other services using XML are possible to test?
Also, awesome test, thanks, it made it super easy to test it 👍
Starting from the bottom just for the sake of reproducibility.
You just said it there, ApiGW does send the "Accept" header as "application/json". In order to reproduce it, you would have to set the header to "application/xml" and you would get the following response: request:
response:
So it seems this was a waste of time :) The problem stems from the issues you mentioned above and I can confirm AWS doesn't support xml transformations, so I wonder if this should be implemented it all. |
3f01af3
to
1239f9c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting that we actually JSON from SQS! Looking at the PR of Alex I've linked earlier, it seems this point was marked as done:
Implement a parser and serializer facade which can dynamically select the right parser/serializer based on the content type / accept headers of the request.
So content negotiation was in fact implemented! Thanks a lot for finding this out, this simplifies the PR a lot!
However, I'm a bit worried about the UpdateIntegration
and UpdateIntegrationResponse
changes, could this break some user setup? What is the current state of those operation? Is it done in moto? Because how I see it, they would now throw exception for most of the calls? Where maybe before it would not have?
I think we have some patching going on for Integration
(apigateway_response_integrations
in patches.py
), so I feel like the fix should either go there instead of implementing something not entirely which may break some Terraform setup, even though I know the current implementation is not great, or then be tested and moved to our provider.
It seems like UpdateIntegrationResponse
might not be implemented at all, so I'm not sure about this one: if they are no tests for it, I'd more in favour to not have it not raise specific exceptions if we're not certain they're being raised. So it would do what you want it do to, but if someone tries to do something that works against AWS but we raise exceptions, that could be a bit blocking.
As an example, I think UpdateIntegration
actually supports more: /cacheKeyParameters
, /requestParameters
and /requestTemplates
. But now we would be raising exception if it's not /requestParameters
only.
if "/requestParameters" not in path: | ||
raise BadRequestException( | ||
f"Invalid patch path '{path}' specified for op '{op}'. Must be one of: [/requestParameters]" | ||
) | ||
if op != "add": | ||
raise BadRequestException( | ||
f"Invalid patch path '{path}' specified for op '{op}'. Please choose supported operations" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is too strict, and would raise Exceptions wen AWS doesn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
too strict because because we don't implement other operations. I followed the same idea of update_resource
you did given your experience in the patch zone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation is good, it's just that the exceptions raised in update_resource
are raised by AWS too, and I'm worried it's not the case here.
if "/responseTemplates" not in path: | ||
raise BadRequestException( | ||
f"Invalid patch path '{path}' specified for op '{op}'. Must be one of: [/requestParameters]" | ||
) | ||
if op != "remove": | ||
raise BadRequestException( | ||
f"Invalid patch path '{path}' specified for op '{op}'. Please choose supported operations" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with this, this is too strict and could break user setup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep it's strict because this is just a basic implementation to make to test pass. And does not implement any other operation.
We do, and I will double check thanks for point that out, but will have to understand why did not pick up the case I had.
It does support more, but we don't I notice this by actually applying this in the test and figure there was no implementation.
Overall, we want to move away from these patches as much as possible. Hard to follow and it's just builds on top of a model we are splitting way anyway
It does support more, no questions about that, but we don't. I figure this my implementing a test that was updating the integration and integration response https://github.com/localstack/localstack/pull/9102/files#diff-357303ef2a04cdb9ff3ebd56f3a8293cd23b26e77a5e8ef0fd72095547076d78R243-R256 |
Totally agree, was suggesting the patch route as there was already an implementation, might not be working but I figured that'd be faster for you. Overall I just don't want to raise exception if we don't support it at the moment and we did not raise exception before. |
80b6439
to
babc8ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for removing the exceptions raised! Just a minor comment regarding the new patch to integration_responses
, I think this might not be needed as we're using the provider for it, but other than that we're good!
@patch(APIGatewayResponse.integration_responses) | ||
def apigateway_response_integration_responses(fn, self, request, *args, **kwargs): | ||
result = fn(self, request, *args, **kwargs) | ||
if self.method not in ["PATCH"]: | ||
return result | ||
|
||
url_path_parts = self.path.split("/") | ||
function_id = url_path_parts[2] | ||
resource_id = url_path_parts[4] | ||
method_type = url_path_parts[6] | ||
|
||
integration = self.backend.get_integration(function_id, resource_id, method_type) | ||
if not integration: | ||
return result | ||
|
||
if self.method == "PATCH": | ||
patch_operations = self._get_param("patchOperations") | ||
apply_json_patch_safe(integration.to_json(), patch_operations, in_place=True) | ||
return 200, {}, json.dumps(integration.to_json()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: do we need this? or is it implemented with update_integration_response
in the provider? I'm not sure I follow as it seems we're patching the whole integration
here and not the integration_response
, and we're patching only the result of to_json()
and not the entity in itself.
It seems from moto this method would be more like:
integration_response = self.backend.get_integration_response(
function_id, resource_id, method_type, status_code
)
I think this might not be called as we would use the provider operation instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤦 no sorry, I fixed the integration and was trying to get the patch for the integration response either but stumbled in a different issue, so I'll remove this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks for addressing all the comments! 🚀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great, glad we were able to get to the bottom of this and come up with a solid fix, after a few iterations. 🚀 Kudos for keeping track of all the conversation in detail, which made it easy to follow the process 🙌
Motivation
Rework this PR to instead of forcing xml to json format exchange, it adds parity to AWS by defaulting SQS responses to JSON. It also works if the request parameter is set to
application/xml
, in that case, the response from the integration is not JSON but XML.Changes
update_integration
andupdate_integration_response
just to comply with the tests. A follow-up PR will improve the implementation.