# -*- coding: utf-8 import datetime try: import ujson as json except ImportError: import json import xml.etree.ElementTree as et import ddt from testtools.matchers import Not, raises import yaml import falcon import falcon.testing as testing class FaultyResource: def on_get(self, req, resp): status = req.get_header('X-Error-Status') title = req.get_header('X-Error-Title') description = req.get_header('X-Error-Description') code = 10042 raise falcon.HTTPError(status, title, description, code=code) def on_post(self, req, resp): raise falcon.HTTPForbidden( 'Request denied', 'You do not have write permissions for this queue.', href='http://example.com/api/rbac') def on_put(self, req, resp): raise falcon.HTTPError( falcon.HTTP_792, 'Internet crashed', 'Catastrophic weather event due to climate change.', href='http://example.com/api/climate', href_text='Drill baby drill!', code=8733224) def on_patch(self, req, resp): raise falcon.HTTPError(falcon.HTTP_400) class UnicodeFaultyResource(object): def __init__(self): self.called = False def on_get(self, req, resp): self.called = True raise falcon.HTTPError( falcon.HTTP_792, u'Internet \xe7rashed!', u'\xc7atastrophic weather event', href=u'http://example.com/api/\xe7limate', href_text=u'Drill b\xe1by drill!') class MiscErrorsResource: def __init__(self, exception, needs_title): self.needs_title = needs_title self._exception = exception def on_get(self, req, resp): if self.needs_title: raise self._exception('Excuse Us', 'Something went boink!') else: raise self._exception('Something went boink!') class UnauthorizedResource: def on_get(self, req, resp): raise falcon.HTTPUnauthorized('Authentication Required', 'Missing or invalid authorization.', ['Basic realm="simple"']) def on_post(self, req, resp): raise falcon.HTTPUnauthorized('Authentication Required', 'Missing or invalid authorization.', ['Newauth realm="apps"', 'Basic realm="simple"']) def on_put(self, req, resp): raise falcon.HTTPUnauthorized('Authentication Required', 'Missing or invalid authorization.', []) class NotFoundResource: def on_get(self, req, resp): raise falcon.HTTPNotFound() class NotFoundResourceWithBody: def on_get(self, req, resp): raise falcon.HTTPNotFound(description='Not Found') class GoneResource: def on_get(self, req, resp): raise falcon.HTTPGone() class GoneResourceWithBody: def on_get(self, req, resp): raise falcon.HTTPGone(description='Gone with the wind') class MethodNotAllowedResource: def on_get(self, req, resp): raise falcon.HTTPMethodNotAllowed(['PUT']) class MethodNotAllowedResourceWithHeaders: def on_get(self, req, resp): raise falcon.HTTPMethodNotAllowed(['PUT'], headers={ 'x-ping': 'pong'}) class MethodNotAllowedResourceWithHeadersWithAccept: def on_get(self, req, resp): raise falcon.HTTPMethodNotAllowed(['PUT'], headers={ 'x-ping': 'pong', 'accept': 'GET,PUT'}) class MethodNotAllowedResourceWithBody: def on_get(self, req, resp): raise falcon.HTTPMethodNotAllowed(['PUT'], description='Not Allowed') class LengthRequiredResource: def on_get(self, req, resp): raise falcon.HTTPLengthRequired('title', 'description') class RequestEntityTooLongResource: def on_get(self, req, resp): raise falcon.HTTPRequestEntityTooLarge('Request Rejected', 'Request Body Too Large') class TemporaryRequestEntityTooLongResource: def __init__(self, retry_after): self.retry_after = retry_after def on_get(self, req, resp): raise falcon.HTTPRequestEntityTooLarge('Request Rejected', 'Request Body Too Large', retry_after=self.retry_after) class UriTooLongResource: def __init__(self, title=None, description=None, code=None): self.title = title self.description = description self.code = code def on_get(self, req, resp): raise falcon.HTTPUriTooLong(self.title, self.description, code=self.code) class RangeNotSatisfiableResource: def on_get(self, req, resp): raise falcon.HTTPRangeNotSatisfiable(123456) class TooManyRequestsResource: def __init__(self, retry_after=None): self.retry_after = retry_after def on_get(self, req, resp): raise falcon.HTTPTooManyRequests('Too many requests', '1 per minute', retry_after=self.retry_after) class ServiceUnavailableResource: def __init__(self, retry_after): self.retry_after = retry_after def on_get(self, req, resp): raise falcon.HTTPServiceUnavailable('Oops', 'Stand by...', retry_after=self.retry_after) class InvalidHeaderResource: def on_get(self, req, resp): raise falcon.HTTPInvalidHeader( 'Please provide a valid token.', 'X-Auth-Token', code='A1001') class MissingHeaderResource: def on_get(self, req, resp): raise falcon.HTTPMissingHeader('X-Auth-Token') class InvalidParamResource: def on_get(self, req, resp): raise falcon.HTTPInvalidParam( 'The value must be a hex-encoded UUID.', 'id', code='P1002') class MissingParamResource: def on_get(self, req, resp): raise falcon.HTTPMissingParam('id', code='P1003') @ddt.ddt class TestHTTPError(testing.TestBase): def before(self): self.resource = FaultyResource() self.api.add_route('/fail', self.resource) def _misc_test(self, exception, status, needs_title=True): self.api.add_route('/misc', MiscErrorsResource(exception, needs_title)) self.simulate_request('/misc') self.assertEqual(self.srmock.status, status) def test_base_class(self): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider.'), 'X-Error-Status': falcon.HTTP_503 } expected_body = { 'title': 'Storage service down', 'description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider.'), 'code': 10042, } # Try it with Accept: */* headers['Accept'] = '*/*' body = self.simulate_request('/fail', headers=headers, decode='utf-8') self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertIn(('vary', 'Accept'), self.srmock.headers) self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) self.assertEqual(expected_body, json.loads(body)) # Now try it with application/json headers['Accept'] = 'application/json' body = self.simulate_request('/fail', headers=headers, decode='utf-8') self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) self.assertEqual(json.loads(body), expected_body) def test_no_description_json(self): body = self.simulate_request('/fail', method='PATCH') self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(body, [json.dumps({'title': '400 Bad Request'}).encode('utf8')]) def test_no_description_xml(self): body = self.simulate_request('/fail', method='PATCH', headers={'Accept': 'application/xml'}) self.assertEqual(self.srmock.status, falcon.HTTP_400) expected_xml = (b'' b'400 Bad Request') self.assertEqual(body, [expected_xml]) def test_client_does_not_accept_json_or_xml(self): headers = { 'Accept': 'application/x-yaml', 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'X-Error-Status': falcon.HTTP_503 } body = self.simulate_request('/fail', headers=headers) self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertEqual(self.srmock.headers_dict['Vary'], 'Accept') self.assertEqual(body, []) def test_custom_old_error_serializer(self): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'X-Error-Status': falcon.HTTP_503 } expected_doc = { 'code': 10042, 'description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'title': 'Storage service down' } def _my_serializer(req, exception): representation = None preferred = req.client_prefers(('application/x-yaml', 'application/json')) if preferred is not None: if preferred == 'application/json': representation = exception.to_json() else: representation = yaml.dump(exception.to_dict(), encoding=None) return (preferred, representation) def _check(media_type, deserializer): headers['Accept'] = media_type self.api.set_error_serializer(_my_serializer) body = self.simulate_request('/fail', headers=headers) self.assertEqual(self.srmock.status, headers['X-Error-Status']) actual_doc = deserializer(body[0].decode('utf-8')) self.assertEqual(expected_doc, actual_doc) _check('application/x-yaml', yaml.load) _check('application/json', json.loads) def test_custom_old_error_serializer_no_body(self): def _my_serializer(req, exception): return (None, None) self.api.set_error_serializer(_my_serializer) self.simulate_request('/fail') def test_custom_new_error_serializer(self): headers = { 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'X-Error-Status': falcon.HTTP_503 } expected_doc = { 'code': 10042, 'description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'title': 'Storage service down' } def _my_serializer(req, resp, exception): representation = None preferred = req.client_prefers(('application/x-yaml', 'application/json')) if preferred is not None: if preferred == 'application/json': representation = exception.to_json() else: representation = yaml.dump(exception.to_dict(), encoding=None) resp.body = representation resp.content_type = preferred def _check(media_type, deserializer): headers['Accept'] = media_type self.api.set_error_serializer(_my_serializer) body = self.simulate_request('/fail', headers=headers) self.assertEqual(self.srmock.status, headers['X-Error-Status']) actual_doc = deserializer(body[0].decode('utf-8')) self.assertEqual(expected_doc, actual_doc) _check('application/x-yaml', yaml.load) _check('application/json', json.loads) def test_client_does_not_accept_anything(self): headers = { 'Accept': '45087gigo;;;;', 'X-Error-Title': 'Storage service down', 'X-Error-Description': ('The configured storage service is not ' 'responding to requests. Please contact ' 'your service provider'), 'X-Error-Status': falcon.HTTP_503 } body = self.simulate_request('/fail', headers=headers) self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertEqual(body, []) @ddt.data( 'application/json', 'application/vnd.company.system.project.resource+json;v=1.1', 'application/json-patch+json', ) def test_forbidden(self, media_type): headers = {'Accept': media_type} expected_body = { 'title': 'Request denied', 'description': ('You do not have write permissions for this ' 'queue.'), 'link': { 'text': 'Documentation related to this error', 'href': 'http://example.com/api/rbac', 'rel': 'help', }, } body = self.simulate_request('/fail', headers=headers, method='POST', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_403) self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) self.assertEqual(json.loads(body), expected_body) def test_epic_fail_json(self): headers = {'Accept': 'application/json'} expected_body = { 'title': 'Internet crashed', 'description': 'Catastrophic weather event due to climate change.', 'code': 8733224, 'link': { 'text': 'Drill baby drill!', 'href': 'http://example.com/api/climate', 'rel': 'help', }, } body = self.simulate_request('/fail', headers=headers, method='PUT', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_792) self.assertThat(lambda: json.loads(body), Not(raises(ValueError))) self.assertEqual(json.loads(body), expected_body) @ddt.data( 'text/xml', 'application/xml', 'application/vnd.company.system.project.resource+xml;v=1.1', 'application/atom+xml', ) def test_epic_fail_xml(self, media_type): headers = {'Accept': media_type} expected_body = ('' + '' + 'Internet crashed' + '' + 'Catastrophic weather event due to climate change.' + '' + '8733224' + '' + 'Drill baby drill!' + 'http://example.com/api/climate' + 'help' + '' + '') body = self.simulate_request('/fail', headers=headers, method='PUT', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_792) self.assertThat(lambda: et.fromstring(body), Not(raises(ValueError))) self.assertEqual(body, expected_body) def test_unicode_json(self): unicode_resource = UnicodeFaultyResource() expected_body = { 'title': u'Internet \xe7rashed!', 'description': u'\xc7atastrophic weather event', 'link': { 'text': u'Drill b\xe1by drill!', 'href': 'http://example.com/api/%C3%A7limate', 'rel': 'help', }, } self.api.add_route('/unicode', unicode_resource) body = self.simulate_request('/unicode', decode='utf-8') self.assertTrue(unicode_resource.called) self.assertEqual(self.srmock.status, falcon.HTTP_792) self.assertEqual(expected_body, json.loads(body)) def test_unicode_xml(self): unicode_resource = UnicodeFaultyResource() expected_body = (u'' + u'' + u'Internet çrashed!' + u'' + u'Çatastrophic weather event' + u'' + u'' + u'Drill báby drill!' + u'http://example.com/api/%C3%A7limate' + u'help' + u'' + u'') self.api.add_route('/unicode', unicode_resource) body = self.simulate_request('/unicode', decode='utf-8', headers={'accept': 'application/xml'}) self.assertTrue(unicode_resource.called) self.assertEqual(self.srmock.status, falcon.HTTP_792) self.assertEqual(expected_body, body) def test_401(self): self.api.add_route('/401', UnauthorizedResource()) self.simulate_request('/401') self.assertEqual(self.srmock.status, falcon.HTTP_401) self.assertIn(('www-authenticate', 'Basic realm="simple"'), self.srmock.headers) self.simulate_request('/401', method='POST') self.assertEqual(self.srmock.status, falcon.HTTP_401) self.assertIn(('www-authenticate', 'Newauth realm="apps", ' 'Basic realm="simple"'), self.srmock.headers) self.simulate_request('/401', method='PUT') self.assertEqual(self.srmock.status, falcon.HTTP_401) self.assertNotIn(('www-authenticate', []), self.srmock.headers) def test_404_without_body(self): self.api.add_route('/404', NotFoundResource()) body = self.simulate_request('/404') self.assertEqual(self.srmock.status, falcon.HTTP_404) self.assertEqual(body, []) def test_404_with_body(self): self.api.add_route('/404', NotFoundResourceWithBody()) response = self.simulate_request('/404', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_404) self.assertNotEqual(response, []) expected_body = { u'title': u'404 Not Found', u'description': u'Not Found' } self.assertEqual(json.loads(response), expected_body) def test_405_without_body(self): self.api.add_route('/405', MethodNotAllowedResource()) response = self.simulate_request('/405') self.assertEqual(self.srmock.status, falcon.HTTP_405) self.assertEqual(response, []) self.assertIn(('allow', 'PUT'), self.srmock.headers) def test_405_without_body_with_extra_headers(self): self.api.add_route('/405', MethodNotAllowedResourceWithHeaders()) response = self.simulate_request('/405') self.assertEqual(self.srmock.status, falcon.HTTP_405) self.assertEqual(response, []) self.assertIn(('allow', 'PUT'), self.srmock.headers) self.assertIn(('x-ping', 'pong'), self.srmock.headers) def test_405_without_body_with_extra_headers_double_check(self): self.api.add_route('/405', MethodNotAllowedResourceWithHeadersWithAccept()) response = self.simulate_request('/405') self.assertEqual(self.srmock.status, falcon.HTTP_405) self.assertEqual(response, []) self.assertIn(('allow', 'PUT'), self.srmock.headers) self.assertNotIn(('allow', 'GET,PUT'), self.srmock.headers) self.assertNotIn(('allow', 'GET'), self.srmock.headers) self.assertIn(('x-ping', 'pong'), self.srmock.headers) def test_405_with_body(self): self.api.add_route('/405', MethodNotAllowedResourceWithBody()) response = self.simulate_request('/405', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_405) self.assertNotEqual(response, []) expected_body = { u'title': u'405 Method Not Allowed', u'description': u'Not Allowed' } self.assertEqual(json.loads(response), expected_body) self.assertIn(('allow', 'PUT'), self.srmock.headers) def test_410_without_body(self): self.api.add_route('/410', GoneResource()) body = self.simulate_request('/410') self.assertEqual(self.srmock.status, falcon.HTTP_410) self.assertEqual(body, []) def test_410_with_body(self): self.api.add_route('/410', GoneResourceWithBody()) response = self.simulate_request('/410', decode='utf-8') self.assertEqual(self.srmock.status, falcon.HTTP_410) self.assertNotEqual(response, []) expected_body = { u'title': u'410 Gone', u'description': u'Gone with the wind' } self.assertEqual(json.loads(response), expected_body) def test_411(self): self.api.add_route('/411', LengthRequiredResource()) body = self.simulate_request('/411') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_411) self.assertEqual(parsed_body['title'], 'title') self.assertEqual(parsed_body['description'], 'description') def test_413(self): self.api.add_route('/413', RequestEntityTooLongResource()) body = self.simulate_request('/413') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_413) self.assertEqual(parsed_body['title'], 'Request Rejected') self.assertEqual(parsed_body['description'], 'Request Body Too Large') self.assertNotIn('retry-after', self.srmock.headers) def test_temporary_413_integer_retry_after(self): self.api.add_route('/413', TemporaryRequestEntityTooLongResource('6')) body = self.simulate_request('/413') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_413) self.assertEqual(parsed_body['title'], 'Request Rejected') self.assertEqual(parsed_body['description'], 'Request Body Too Large') self.assertIn(('retry-after', '6'), self.srmock.headers) def test_temporary_413_datetime_retry_after(self): date = datetime.datetime.now() + datetime.timedelta(minutes=5) self.api.add_route('/413', TemporaryRequestEntityTooLongResource(date)) body = self.simulate_request('/413') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_413) self.assertEqual(parsed_body['title'], 'Request Rejected') self.assertEqual(parsed_body['description'], 'Request Body Too Large') self.assertIn(('retry-after', falcon.util.dt_to_http(date)), self.srmock.headers) def test_414(self): self.api.add_route('/414', UriTooLongResource()) self.simulate_request('/414') self.assertEqual(self.srmock.status, falcon.HTTP_414) def test_414_with_title(self): title = 'Argh! Error!' self.api.add_route('/414', UriTooLongResource(title=title)) body = self.simulate_request('/414', headers={}) parsed_body = json.loads(body[0].decode()) self.assertEqual(parsed_body['title'], title) def test_414_with_description(self): description = 'Be short please.' self.api.add_route('/414', UriTooLongResource(description=description)) body = self.simulate_request('/414', headers={}) parsed_body = json.loads(body[0].decode()) self.assertEqual(parsed_body['description'], description) def test_414_with_custom_kwargs(self): code = 'someid' self.api.add_route('/414', UriTooLongResource(code=code)) body = self.simulate_request('/414', headers={}) parsed_body = json.loads(body[0].decode()) self.assertEqual(parsed_body['code'], code) def test_416(self): self.api = falcon.API() self.api.add_route('/416', RangeNotSatisfiableResource()) body = self.simulate_request('/416', headers={'accept': 'text/xml'}) self.assertEqual(self.srmock.status, falcon.HTTP_416) self.assertEqual(body, []) self.assertIn(('content-range', 'bytes */123456'), self.srmock.headers) self.assertIn(('content-length', '0'), self.srmock.headers) def test_429_no_retry_after(self): self.api.add_route('/429', TooManyRequestsResource()) body = self.simulate_request('/429') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_429) self.assertEqual(parsed_body['title'], 'Too many requests') self.assertEqual(parsed_body['description'], '1 per minute') self.assertNotIn('retry-after', self.srmock.headers) def test_429(self): self.api.add_route('/429', TooManyRequestsResource(60)) body = self.simulate_request('/429') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_429) self.assertEqual(parsed_body['title'], 'Too many requests') self.assertEqual(parsed_body['description'], '1 per minute') self.assertIn(('retry-after', '60'), self.srmock.headers) def test_429_datetime(self): date = datetime.datetime.now() + datetime.timedelta(minutes=1) self.api.add_route('/429', TooManyRequestsResource(date)) body = self.simulate_request('/429') parsed_body = json.loads(body[0].decode()) self.assertEqual(self.srmock.status, falcon.HTTP_429) self.assertEqual(parsed_body['title'], 'Too many requests') self.assertEqual(parsed_body['description'], '1 per minute') self.assertIn(('retry-after', falcon.util.dt_to_http(date)), self.srmock.headers) def test_503_integer_retry_after(self): self.api.add_route('/503', ServiceUnavailableResource(60)) body = self.simulate_request('/503', decode='utf-8') expected_body = { u'title': u'Oops', u'description': u'Stand by...', } self.assertEqual(self.srmock.status, falcon.HTTP_503) self.assertEqual(json.loads(body), expected_body) self.assertIn(('retry-after', '60'), self.srmock.headers) def test_503_datetime_retry_after(self): date = datetime.datetime.now() + datetime.timedelta(minutes=5) self.api.add_route('/503', ServiceUnavailableResource(date)) body = self.simulate_request('/503', decode='utf-8') expected_body = { u'title': u'Oops', u'description': u'Stand by...', } self.assertEqual(self.srmock.status, falcon.HTTP_503) self.assertEqual(json.loads(body), expected_body) self.assertIn(('retry-after', falcon.util.dt_to_http(date)), self.srmock.headers) def test_invalid_header(self): self.api.add_route('/400', InvalidHeaderResource()) body = self.simulate_request('/400', decode='utf-8') expected_desc = (u'The value provided for the X-Auth-Token ' u'header is invalid. Please provide a valid token.') expected_body = { u'title': u'Invalid header value', u'description': expected_desc, u'code': u'A1001', } self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(json.loads(body), expected_body) def test_missing_header(self): self.api.add_route('/400', MissingHeaderResource()) body = self.simulate_request('/400', decode='utf-8') expected_body = { u'title': u'Missing header value', u'description': u'The X-Auth-Token header is required.', } self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(json.loads(body), expected_body) def test_invalid_param(self): self.api.add_route('/400', InvalidParamResource()) body = self.simulate_request('/400', decode='utf-8') expected_desc = (u'The "id" parameter is invalid. The ' u'value must be a hex-encoded UUID.') expected_body = { u'title': u'Invalid parameter', u'description': expected_desc, u'code': u'P1002', } self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(json.loads(body), expected_body) def test_missing_param(self): self.api.add_route('/400', MissingParamResource()) body = self.simulate_request('/400', decode='utf-8') expected_body = { u'title': u'Missing parameter', u'description': u'The "id" parameter is required.', u'code': u'P1003', } self.assertEqual(self.srmock.status, falcon.HTTP_400) self.assertEqual(json.loads(body), expected_body) def test_misc(self): self._misc_test(falcon.HTTPBadRequest, falcon.HTTP_400) self._misc_test(falcon.HTTPNotAcceptable, falcon.HTTP_406, needs_title=False) self._misc_test(falcon.HTTPConflict, falcon.HTTP_409) self._misc_test(falcon.HTTPPreconditionFailed, falcon.HTTP_412) self._misc_test(falcon.HTTPUnsupportedMediaType, falcon.HTTP_415, needs_title=False) self._misc_test(falcon.HTTPUnprocessableEntity, falcon.HTTP_422) self._misc_test(falcon.HTTPUnavailableForLegalReasons, falcon.HTTP_451, needs_title=False) self._misc_test(falcon.HTTPInternalServerError, falcon.HTTP_500) self._misc_test(falcon.HTTPBadGateway, falcon.HTTP_502) def test_title_default_message_if_none(self): headers = { 'X-Error-Status': falcon.HTTP_503 } body = self.simulate_request('/fail', headers=headers, decode='utf-8') body_json = json.loads(body) self.assertEqual(self.srmock.status, headers['X-Error-Status']) self.assertEqual(body_json['title'], headers['X-Error-Status'])