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
DELETE requests should return specified Content-Type #85
Conversation
Currently I am returning an empty string if the response is 204, can we write a test to make sure this behaviour still works. It was the only way to make it set the |
tests/test_rebar.py
Outdated
@@ -248,6 +248,46 @@ def get_me(): | |||
) | |||
self.assertEqual(resp.status_code, 400) | |||
|
|||
def test_validate_headers_delete(self): |
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.
if you add a docstring comment the testrunner output is going to improve in readability.
Not a blocking issue though
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
oh well, I guess my review doesn't count;) |
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 am not sure this is correct.
If no schema is defined, then we should not be assuming the structure of the response. It seems like it could be a legitimate use case to return a string that is "text/html" or any other content type.
This would be a breaking change for such a case.
The entire rebar system builds off the definition of explicit schemas for every potential response type.
I would question why the "bad" DELETE responses are not correctly defining a response schema, as that should address this.
Unless I am misunderstanding the issue?
Edit: I should clarify. I don't actually think returning anything other than a JSON from a API wrapped with rebar is a legitimate case we should support, but it is a case we currently support (to the best of my knowledge). I think it should be a very clear breaking change if we start enforcing JSON format returns rather than quietly attempting to dump them to a json.
My thinking is that our response header should honor what our registry |
@joeb1415 That would make sense to me, but (as far as I can see)
I think that it should then be safe to either set the 'Content-Type' header based on what the registry 'produces', or at least error out when it does not match. That should solve what seems to be the root of your issue which is that we are relying on Flask's default mime type tools to set a header whose value our clients rely on being a certain value. |
@joeb1415 Looking into it a bit more, you could also modify your flask app to resolve this by changing the |
Is it not fair to standardize all responses as Content-Type JSON? This was the case prior to the commit I linked above, I feel like it's more of an accident that some return HTML now. FWIW, v3 doesn't accept a I'll take a quick look at adding an explicit control of this but I'm curious if it's actually a regression to change the current implementation back to JSON. |
I tried adding a |
I like it, it's clean. Maybe rebar could provide an enum or something similar with all the content types? |
Yes totally fair, but then I think we also should be standardizing on all responses defining a Schema. I just don't agree with the middle ground of not enforcing a schema but enforcing a content type. |
flask_rebar/rebar.py
Outdated
@@ -102,6 +103,7 @@ def _wrap_handler( | |||
request_body_schema=None, | |||
headers_schema=None, | |||
marshal_schema=None, | |||
produces=None, |
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'm not a fan of this interface.
Its conflated with "produces" from OpenAPI 2.0.
Also, this doesn't actually guarantee that the handler actually returns the content-type specified here. It just patches the headers.
So maybe this should be a "headers" parameter, with extra headers to set on the response? That seems more flexible and more clear.
tests/test_rebar.py
Outdated
def delete_me(): | ||
return None, 204 | ||
|
||
# Test DELETE with non-None return schema |
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.
Can we make this a separate test method?
tests/test_rebar.py
Outdated
self.assertEqual(resp.status_code, 204) | ||
self.assertEqual(resp.data.decode("utf-8"), "") | ||
self.assertEqual( | ||
next(header[1] for header in resp.headers.to_list() if header[0] == "Content-Type"), |
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 feel like it should be possible to do resp.headers['Content-Type']
, or something similar?
Chiming in a little late here: There should be no "content-type" header when there is no content. However, its customary to add this header regardless to support clients that incorrectly attempt to parse this header. Given that flask-rebar generally returns JSON, it seems totally reasonable to just make it default to "application/json" when there is no content. With that - I think this is more reason to add actual content negotiation. |
tests/test_rebar.py
Outdated
self.assertEqual(resp.status_code, 204) | ||
self.assertEqual(resp.data.decode("utf-8"), "") | ||
self.assertEqual( | ||
next(header[1] for header in resp.headers.to_list() if header[0] == "Content-Type"), |
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.
As above. e.g. https://werkzeug.palletsprojects.com/en/0.14.x/quickstart/ includes the example
>>> response.headers['content-type']
'text/plain; charset=utf-8'
Looks like werkzeug will always pass a Good thoughts on the more flexible |
@joeb1415 FWIW, its also possible to set the headers with the return value: https://github.com/plangrid/flask-rebar/blob/master/flask_rebar/rebar.py#L66 |
@barakalon mind taking another look to let me know if you still have changes or if we're ✅ ? |
Gonna go ahead and merge this one and push out a patch version to close out incident - Opening a new issue #91 to hash out some of the finer details of content-type negotiation |
@@ -59,8 +65,17 @@ def response(data, status_code=200, headers=None): | |||
:rtype: flask.Response | |||
""" | |||
resp = jsonify(data) | |||
|
|||
if not get_json_from_resp(resp=resp): |
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.
This seems like a lot of unnecessary processing (i.e. we're loading all responses into memory, even potentially very large ones). It also is strange behavior, in that it seems to be catering to another part of our code that isn't clear from just the context of this function.
What's the reasoning here?
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.
Also, what if a developer actually wants to return an empty object? This will lead to very confusing behavior - it will replace their empty object with an empty string.
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.
Or possibly even more possible, what if a developer wants to return an empty list?
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.
Again, matching existing behavior:
https://github.com/plangrid/flask-rebar/blob/master/tests/test_rebar.py#L319
Would make sense to change that, but that would be a change from what we had previously.
Could save the processing here by just checking for other various almost-falsey cases. b""
or b"{}"
don't evaluate to false, but do when decoded.
|
||
schema = marshal_schema[status_code] # May raise KeyError. | ||
|
||
# The schema may be declared as None to bypass marshaling (e.g. for 204 responses). | ||
if schema is None: | ||
return make_response((data or "", status_code, headers)) | ||
return response(data=data or {}, status_code=status_code, headers=headers) |
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 see that we later replace {}
with an empty string... but I'm not sure why?
An empty object is not empty content. I.e. if I have a marshal schema of {204: None}, this looks like it adds '{}' to the response content. Which wouldn't be right.
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 replaced with an empty string to match the existing unit test:
https://github.com/plangrid/flask-rebar/blob/master/tests/test_rebar.py#L319
Sorry for the delay @joeb1415 I think this introduced some subtle bugs in the way it handles "falsey" responses. |
@barakalon no worries! I think these changes were consistent, but let me know if you think otherwise. Also if we want to actually change what we return in the None schema case, I'm open to that too. |
@joeb1415 sorry, I'm going to push back again By moving the logic into the underlying |
@barakalon hmm, what do you make of the test I linked to there? We had already been returning an empty string when |
Try adding a test where the schema is not I would expect the response body to be |
Gotcha. I added this test here, give it a check! |
DELETE requests are returning
Content-Type: text/html
, this is breaking some clients that expectapplication/json
, even though there is no actual content, simply the type in the header broke a lookup. Looks like this was introduced here, astext/html
is the Flask response default mimetype.Rolling that back here and adding a test.