Handle CSRFErrors as a subset of 400 errors #74
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Trello: https://trello.com/c/1K5ePclo/372-your-session-has-expired-instead-of-ugly-csrf-message
Redirects the user to the login page with an accompanying flash message. We can move this to utils in a separate PR.
TLDR: Flask is doing error handling weirdly, we can work around it by catching all 400s but only handling CSRFErrors.
Long version:
I have a theory why Flask won't recognise the
CSRFError
in its own handler. The recommended advice from Flask-WTF (http://flask-wtf.readthedocs.io/en/stable/csrf.html) is to do something like this:(We actually define our error handlers on the blueprint, not the application, however this is just a wrapper for the app functionality, and shouldn't make a difference.)
A bit of background on Flask error handling sheds some light:
try.. except
somewhere, it goes to Flask's error handling athandle_exception
(https://github.com/pallets/flask/blob/0.12.4/flask/app.py#L1520).current_app.error_handler_spec
, and split into HTTP status code handlers (using 404, 500 etc as the key) and a list of any user-defined ones (e.g. for Brief Responses we defineAPIError
andQuestionNotFoundError
), with the snappily-named key 'None' for that list. An exampleerror_handler_spec
looks like this:None
, it compares the exception classes in the tuples._find_error_handler
function looks for a handler using a status code if there is one (https://github.com/pallets/flask/blob/0.12.4/flask/app.py#L1429), so if an exception has a status code but its handler is defined by its class, that handler will never get called. 馃樋It turns out that
APIError
andQuestionNotFoundError
are not based on HTTP exceptions, whereasCSRFError
is a subclass ofBadRequest
(400 status). There's a check here https://github.com/pallets/flask/blob/0.12.4/flask/app.py#L1110 for the exception subclass that manages to wrangle a 400 status code out of theCSRFError
. It uses that code to look up the 400 handler in the dictionary and ignores our custom handler.So to solve this for now, I decided to go with the flow and have my handler listen for all 400s and ignore anything that wasn't a CSRF.
Unless I've missed something daft, this feels like a bug in Flask. Or perhaps Flask-WTF's advice is just misguided. Thoughts welcome.