-
Notifications
You must be signed in to change notification settings - Fork 8
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
Error payload #186
Error payload #186
Changes from all commits
0889ea1
5c4fe7f
9764797
eeda70b
25caf30
dabad45
fb137ac
8b5909b
873af13
35f42e4
424e572
138f8fd
cc26710
61b10f4
ba04eaf
b70230b
0ac6eae
6e5ed8e
7bb495d
901f0d2
84df4e9
246113d
e919598
da7287f
446fe7e
64ea0b1
869230a
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,30 @@ | ||
remoteappmanager.rest.http package | ||
================================== | ||
|
||
Submodules | ||
---------- | ||
|
||
remoteappmanager.rest.http.httpstatus module | ||
-------------------------------------------- | ||
|
||
.. automodule:: remoteappmanager.rest.http.httpstatus | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: | ||
|
||
remoteappmanager.rest.http.payloaded_http_error module | ||
------------------------------------------------------ | ||
|
||
.. automodule:: remoteappmanager.rest.http.payloaded_http_error | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: | ||
|
||
|
||
Module contents | ||
--------------- | ||
|
||
.. automodule:: remoteappmanager.rest.http | ||
:members: | ||
:undoc-members: | ||
:show-inheritance: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from tornado.web import HTTPError | ||
|
||
|
||
class PayloadedHTTPError(HTTPError): | ||
def __init__(self, status_code, | ||
payload=None, | ||
content_type=None, | ||
log_message=None, | ||
*args, **kwargs): | ||
"""Provides a HTTPError that contains a string payload to output | ||
as a response. If the payload is None, behaves like a regular | ||
HTTPError, producing no payload in the response. | ||
|
||
Parameters | ||
---------- | ||
payload: str or None | ||
The payload as a string | ||
content_type: str or None | ||
The content type of the payload | ||
log_message: str or None | ||
The log message. Passed to the HTTPError. | ||
""" | ||
super().__init__(status_code, log_message, *args, **kwargs) | ||
|
||
if payload is not None: | ||
if not isinstance(payload, str): | ||
raise ValueError("payload must be a string.") | ||
|
||
if content_type is None: | ||
content_type = "text/plain" | ||
else: | ||
if content_type is not None: | ||
raise ValueError("Content type specified, but no payload") | ||
|
||
self.content_type = content_type | ||
self.payload = payload |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,10 @@ | ||
from remoteappmanager.handlers.base_handler import BaseHandler | ||
from tornado import gen, web, escape | ||
|
||
from remoteappmanager.rest import exceptions | ||
from remoteappmanager.rest.http import httpstatus | ||
from remoteappmanager.rest.http.payloaded_http_error import PayloadedHTTPError | ||
from remoteappmanager.rest.registry import registry | ||
from remoteappmanager.utils import url_path_join, with_end_slash | ||
from remoteappmanager.rest import httpstatus, exceptions | ||
from tornado import gen, web, escape | ||
|
||
|
||
class RESTBaseHandler(BaseHandler): | ||
|
@@ -25,6 +26,44 @@ def get_resource_handler_or_404(self, collection_name): | |
except KeyError: | ||
raise web.HTTPError(httpstatus.NOT_FOUND) | ||
|
||
def write_error(self, status_code, **kwargs): | ||
"""Provides appropriate payload to the response in case of error. | ||
""" | ||
exc_info = kwargs.get("exc_info") | ||
|
||
if exc_info is None: | ||
self.clear_header('Content-Type') | ||
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. 'Content-Type' is by default not set unless you overload 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. write error is responsible for producing the right output. The content type depends on the payload. We know the payload here, so we zero the content-type here. |
||
self.finish() | ||
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. finish with the 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. the status code is set somewhere else. write error is just in charge of rendering the payload. |
||
|
||
exc = exc_info[1] | ||
|
||
if isinstance(exc, PayloadedHTTPError) and exc.payload is not None: | ||
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. Alternatively, convert 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 part of the design decision. My vision is that this method only handles http-level exception types, with the conversion of exception type done at the individual http method handlers. This allows us to take appropriate action and, for example log contextually to the method handler. Converting here we lose this specificity. I don't know which one is best. For sure, once we were to go for a write_error method that handles rest exceptions, then it should handle all exceptions. Plus, write_error is just responsible for the payload. We would have to reimplement send_error too, so that proper status can be set. 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.
We can't prevent non-http-level exception from arriving at write_error. write_error is intended to be able to handle BaseException in general.
I don't think converting a RestException to PayloadedHTTPError here stop you from logging contextually. If you must log, you can do so in Rest*Handler
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. write an issue 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. Do you mean opening an issue for logging? I only meant to say that 'converting a RestException to PayloadedHTTPError does not stop you from logging contextually', I don't mean that there is an issue with logging. 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. If we want to handle exceptions at the write_error level I need to do some adjustments to the current code. My main concern is if it's a good idea to handle a broad range of exceptions types there. |
||
self.set_header('Content-Type', exc.content_type) | ||
self.finish(exc.payload) | ||
else: | ||
# For non-payloaded http errors or any other exception | ||
# we don't want to return anything as payload. | ||
# The error code is enough. | ||
self.clear_header('Content-Type') | ||
self.finish() | ||
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. If I am correct, we need to call 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. finish does not take the status code. 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. Sorry I meant to have the status code in the response body as text, not the response.status_code attribute. 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. It is not necessary but convenient to see the status code displayed on the page. |
||
|
||
def rest_to_http_exception(self, rest_exc): | ||
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. Class method for PayloadedHTTPError? 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. No. In the future, the conversion will be done by a serializer that may produce xml instead of json. This serializer will be a instance var. |
||
"""Converts a REST exception into the appropriate HTTP one.""" | ||
|
||
representation = rest_exc.representation() | ||
payload = None | ||
content_type = None | ||
|
||
if representation is not None: | ||
payload = escape.json_encode(representation) | ||
content_type = "application/json" | ||
|
||
return PayloadedHTTPError( | ||
status_code=rest_exc.http_code, | ||
payload=payload, | ||
content_type=content_type | ||
) | ||
|
||
|
||
class RESTCollectionHandler(RESTBaseHandler): | ||
"""Handler for URLs addressing a collection. | ||
|
@@ -38,7 +77,7 @@ def get(self, collection_name): | |
try: | ||
items = yield res_handler.items() | ||
except exceptions.RESTException as e: | ||
raise web.HTTPError(e.http_code) | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -67,7 +106,7 @@ def post(self, collection_name): | |
try: | ||
resource_id = yield res_handler.create(data) | ||
except exceptions.RESTException as e: | ||
raise web.HTTPError(e.http_code) | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -87,6 +126,7 @@ def post(self, collection_name): | |
|
||
self.set_status(httpstatus.CREATED) | ||
self.set_header("Location", location) | ||
self.clear_header('Content-Type') | ||
self.flush() | ||
|
||
|
||
|
@@ -104,7 +144,7 @@ def get(self, collection_name, identifier): | |
try: | ||
representation = yield res_handler.retrieve(identifier) | ||
except exceptions.RESTException as e: | ||
raise web.HTTPError(e.http_code) | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -128,6 +168,8 @@ def post(self, collection_name, identifier): | |
|
||
try: | ||
exists = yield res_handler.exists(identifier) | ||
except exceptions.RESTException as e: | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -156,7 +198,7 @@ def put(self, collection_name, identifier): | |
try: | ||
yield res_handler.update(identifier, representation) | ||
except exceptions.RESTException as e: | ||
raise web.HTTPError(e.http_code) | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -166,6 +208,7 @@ def put(self, collection_name, identifier): | |
identifier)) | ||
raise web.HTTPError(httpstatus.INTERNAL_SERVER_ERROR) | ||
|
||
self.clear_header('Content-Type') | ||
self.set_status(httpstatus.NO_CONTENT) | ||
|
||
@web.authenticated | ||
|
@@ -176,7 +219,7 @@ def delete(self, collection_name, identifier): | |
try: | ||
yield res_handler.delete(identifier) | ||
except exceptions.RESTException as e: | ||
raise web.HTTPError(e.http_code) | ||
raise self.rest_to_http_exception(e) | ||
except NotImplementedError: | ||
raise web.HTTPError(httpstatus.METHOD_NOT_ALLOWED) | ||
except Exception: | ||
|
@@ -186,4 +229,5 @@ def delete(self, collection_name, identifier): | |
identifier)) | ||
raise web.HTTPError(httpstatus.INTERNAL_SERVER_ERROR) | ||
|
||
self.clear_header('Content-Type') | ||
self.set_status(httpstatus.NO_CONTENT) |
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.
Why is this a problem?
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.
Why would anyone say "application/json" and then provide no payload?
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.
It is not what you would expect, but does it actually cause problem? If not, seem to be an unnecessary potential failure 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.
It is not unnecessary. The class constraints are clear. if you specify the payload, you can or cannot specify the content type. If you don't, it's by default text/plain. If you don't specify a payload, there's no reason for specifying the content type, and if you do, you are doing something very wrong, because it means that your payload is None by accident.
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.
Is it wrong to set the header content-type to something but provide an empty content?
(by the way you could default
payload
to an empty string, just an idea)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.
There's a difference between empty payload an no payload. some http responses don't want payload, and it's invalid if they do provide one (e.g. 204 No content, or 404 Not Found)
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.
At least in the context of REST, I mean. 404 can have a payload in non-rest context
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 payload but has content-type header - is it just because it is wrong in terms of definition or does it cause error?
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.
it causes an error.
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.
Would you explain how please? I just wanted to understand.