diff --git a/.editorconfig b/.editorconfig index ae54f90b0b4..f42760bed80 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,3 +17,6 @@ indent_style = tab [*.{yml,yaml}] indent_size = 2 + +[*.rst] +max_line_length = 80 diff --git a/CHANGES/3462.feature b/CHANGES/3462.feature new file mode 100644 index 00000000000..275b0a907ae --- /dev/null +++ b/CHANGES/3462.feature @@ -0,0 +1 @@ +``web.HTTPException`` and derived classes are not inherited from ``web.Response`` anymore. diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py index 9768c4f6407..4b56f38edf3 100644 --- a/aiohttp/web_exceptions.py +++ b/aiohttp/web_exceptions.py @@ -1,8 +1,12 @@ import warnings -from typing import Any, Dict, Iterable, List, Optional, Set # noqa +from http import HTTPStatus +from typing import Any, Dict, Iterable, List, Optional, Set, Type, cast # noqa +from multidict import CIMultiDict +from yarl import URL + +from . import hdrs from .typedefs import LooseHeaders, StrOrURL -from .web_response import Response __all__ = ( 'HTTPException', @@ -69,36 +73,77 @@ # HTTP Exceptions ############################################################ -class HTTPException(Response, Exception): +class HTTPException(Exception): # You should set in subclasses: # status = 200 status_code = -1 empty_body = False + default_reason = "" # Initialized at the end of the module __http_exception__ = True def __init__(self, *, headers: Optional[LooseHeaders]=None, reason: Optional[str]=None, - body: Any=None, text: Optional[str]=None, content_type: Optional[str]=None) -> None: - if body is not None: - warnings.warn( - "body argument is deprecated for http web exceptions", - DeprecationWarning) - Response.__init__(self, status=self.status_code, - headers=headers, reason=reason, - body=body, text=text, content_type=content_type) - Exception.__init__(self, self.reason) - if self.body is None and not self.empty_body: - self.text = "{}: {}".format(self.status, self.reason) + if reason is None: + reason = self.default_reason + + if text is None: + if not self.empty_body: + text = "{}: {}".format(self.status_code, reason) + else: + if self.empty_body: + warnings.warn( + "text argument is deprecated for HTTP status {} " + "in aiohttp 4.0 (#3462)," + "the response should be provided without a body".format( + self.status_code), + DeprecationWarning, + stacklevel=2) + + if headers is not None: + real_headers = CIMultiDict(headers) + else: + real_headers = CIMultiDict() + + if content_type is not None: + if not text: + warnings.warn("content_type without text is deprecated " + "in aiohttp 4.0 (#3462)", + DeprecationWarning, + stacklevel=2) + real_headers[hdrs.CONTENT_TYPE] = content_type + elif hdrs.CONTENT_TYPE not in real_headers and text: + real_headers[hdrs.CONTENT_TYPE] = 'text/plain' + + super().__init__(reason) + + self._text = text + self._headers = real_headers def __bool__(self) -> bool: return True + @property + def status(self) -> int: + return self.status_code + + @property + def reason(self) -> str: + return self.args[0] + + @property + def text(self) -> Optional[str]: + return self._text + + @property + def headers(self) -> 'CIMultiDict[str]': + return self._headers + class HTTPError(HTTPException): """Base class for exceptions with status codes in the 400s and 500s.""" @@ -147,39 +192,42 @@ class HTTPPartialContent(HTTPSuccessful): ############################################################ -class _HTTPMove(HTTPRedirection): +class HTTPMove(HTTPRedirection): def __init__(self, location: StrOrURL, *, headers: Optional[LooseHeaders]=None, reason: Optional[str]=None, - body: Any=None, text: Optional[str]=None, content_type: Optional[str]=None) -> None: if not location: raise ValueError("HTTP redirects need a location to redirect to.") super().__init__(headers=headers, reason=reason, - body=body, text=text, content_type=content_type) + text=text, content_type=content_type) self.headers['Location'] = str(location) - self.location = location + self._location = URL(location) + + @property + def location(self) -> URL: + return self._location -class HTTPMultipleChoices(_HTTPMove): +class HTTPMultipleChoices(HTTPMove): status_code = 300 -class HTTPMovedPermanently(_HTTPMove): +class HTTPMovedPermanently(HTTPMove): status_code = 301 -class HTTPFound(_HTTPMove): +class HTTPFound(HTTPMove): status_code = 302 # This one is safe after a POST (the redirected location will be # retrieved with GET): -class HTTPSeeOther(_HTTPMove): +class HTTPSeeOther(HTTPMove): status_code = 303 @@ -189,16 +237,16 @@ class HTTPNotModified(HTTPRedirection): empty_body = True -class HTTPUseProxy(_HTTPMove): +class HTTPUseProxy(HTTPMove): # Not a move, but looks a little like one status_code = 305 -class HTTPTemporaryRedirect(_HTTPMove): +class HTTPTemporaryRedirect(HTTPMove): status_code = 307 -class HTTPPermanentRedirect(_HTTPMove): +class HTTPPermanentRedirect(HTTPMove): status_code = 308 @@ -240,15 +288,22 @@ def __init__(self, *, headers: Optional[LooseHeaders]=None, reason: Optional[str]=None, - body: Any=None, text: Optional[str]=None, content_type: Optional[str]=None) -> None: allow = ','.join(sorted(allowed_methods)) super().__init__(headers=headers, reason=reason, - body=body, text=text, content_type=content_type) + text=text, content_type=content_type) self.headers['Allow'] = allow - self.allowed_methods = set(allowed_methods) # type: Set[str] - self.method = method.upper() + self._allowed = set(allowed_methods) # type: Set[str] + self._method = method + + @property + def allowed_methods(self) -> Set[str]: + return self._allowed + + @property + def method(self) -> str: + return self._method class HTTPNotAcceptable(HTTPClientError): @@ -283,8 +338,8 @@ class HTTPRequestEntityTooLarge(HTTPClientError): status_code = 413 def __init__(self, - max_size: float, - actual_size: float, + max_size: int, + actual_size: int, **kwargs: Any) -> None: kwargs.setdefault( 'text', @@ -342,17 +397,20 @@ class HTTPUnavailableForLegalReasons(HTTPClientError): status_code = 451 def __init__(self, - link: str, + link: StrOrURL, *, headers: Optional[LooseHeaders]=None, reason: Optional[str]=None, - body: Any=None, text: Optional[str]=None, content_type: Optional[str]=None) -> None: super().__init__(headers=headers, reason=reason, - body=body, text=text, content_type=content_type) - self.headers['Link'] = '<%s>; rel="blocked-by"' % link - self.link = link + text=text, content_type=content_type) + self.headers['Link'] = '<{}>; rel="blocked-by"'.format(str(link)) + self._link = URL(link) + + @property + def link(self) -> URL: + return self._link ############################################################ @@ -409,3 +467,19 @@ class HTTPNotExtended(HTTPServerError): class HTTPNetworkAuthenticationRequired(HTTPServerError): status_code = 511 + + +def _initialize_default_reason() -> None: + for obj in globals().values(): + if isinstance(obj, type) and issubclass(obj, HTTPException): + exc = cast(Type[HTTPException], obj) + if exc.status_code >= 0: + try: + status = HTTPStatus(exc.status_code) + exc.default_reason = status.phrase + except ValueError: + pass + + +_initialize_default_reason() +del _initialize_default_reason diff --git a/aiohttp/web_middlewares.py b/aiohttp/web_middlewares.py index 7c2e7a41458..a6a444b42d4 100644 --- a/aiohttp/web_middlewares.py +++ b/aiohttp/web_middlewares.py @@ -1,7 +1,7 @@ import re from typing import TYPE_CHECKING, Awaitable, Callable, Tuple, Type, TypeVar -from .web_exceptions import HTTPMovedPermanently, _HTTPMove +from .web_exceptions import HTTPMove, HTTPMovedPermanently from .web_request import Request from .web_response import StreamResponse from .web_urldispatcher import SystemRoute @@ -42,7 +42,7 @@ def middleware(f: _Func) -> _Func: def normalize_path_middleware( *, append_slash: bool=True, remove_slash: bool=False, merge_slashes: bool=True, - redirect_class: Type[_HTTPMove]=HTTPMovedPermanently) -> _Middleware: + redirect_class: Type[HTTPMove]=HTTPMovedPermanently) -> _Middleware: """ Middleware factory which produces a middleware that normalizes the path of a request. By normalizing it means: diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py index c736a78ef8c..349ab4c5a10 100644 --- a/aiohttp/web_protocol.py +++ b/aiohttp/web_protocol.py @@ -417,7 +417,10 @@ async def start(self) -> None: self._request_handler(request)) resp = await task except HTTPException as exc: - resp = exc + resp = Response(status=exc.status, + reason=exc.reason, + text=exc.text, + headers=exc.headers) except asyncio.CancelledError: self.log_debug('Ignored premature client disconnection') break diff --git a/docs/web.rst b/docs/web.rst index 4fab23d0067..6ec00026664 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -16,6 +16,7 @@ The page contains all information about aiohttp Server API: Advanced Usage Low Level Reference + Web Exceptions Logging Testing Deployment diff --git a/docs/web_exceptions.rst b/docs/web_exceptions.rst new file mode 100644 index 00000000000..46a8a6eb5c3 --- /dev/null +++ b/docs/web_exceptions.rst @@ -0,0 +1,465 @@ +.. _aiohttp-web-exceptions: + +Web Server Exceptions +===================== + +.. currentmodule:: aiohttp.web + +Overview +-------- + +:mod:`aiohttp.web` defines a set of exceptions for every *HTTP status code*. + +Each exception is a subclass of :exc:`HTTPException` and relates to a single +HTTP status code:: + + async def handler(request): + raise aiohttp.web.HTTPFound('/redirect') + +Each exception class has a status code according to :rfc:`2068`: +codes with 100-300 are not really errors; 400s are client errors, +and 500s are server errors. + +HTTP Exception hierarchy chart:: + + Exception + HTTPException + HTTPSuccessful + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 307 - HTTPTemporaryRedirect + * 308 - HTTPPermanentRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + * 421 - HTTPMisdirectedRequest + * 422 - HTTPUnprocessableEntity + * 424 - HTTPFailedDependency + * 426 - HTTPUpgradeRequired + * 428 - HTTPPreconditionRequired + * 429 - HTTPTooManyRequests + * 431 - HTTPRequestHeaderFieldsTooLarge + * 451 - HTTPUnavailableForLegalReasons + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + * 506 - HTTPVariantAlsoNegotiates + * 507 - HTTPInsufficientStorage + * 510 - HTTPNotExtended + * 511 - HTTPNetworkAuthenticationRequired + +All HTTP exceptions have the same constructor signature:: + + HTTPNotFound(*, headers=None, reason=None, + body=None, text=None, content_type=None) + +If not directly specified, *headers* will be added to the *default +response headers*. + +Classes :exc:`HTTPMultipleChoices`, :exc:`HTTPMovedPermanently`, +:exc:`HTTPFound`, :exc:`HTTPSeeOther`, :exc:`HTTPUseProxy`, +:exc:`HTTPTemporaryRedirect` have the following constructor signature:: + + HTTPFound(location, *, headers=None, reason=None, + body=None, text=None, content_type=None) + +where *location* is value for *Location HTTP header*. + +:exc:`HTTPMethodNotAllowed` is constructed by providing the incoming +unsupported method and list of allowed methods:: + + HTTPMethodNotAllowed(method, allowed_methods, *, + headers=None, reason=None, + body=None, text=None, content_type=None) + +Base HTTP Exception +------------------- + +.. exception:: HTTPException(*, headers=None, reason=None, text=None, \ + content_type=None) + + The base class for HTTP server exceptions. Inherited from :exc:`Exception`. + + :param headers: HTTP headers (:class:`~collections.abc.Mapping`) + + :param str reason: an optional custom HTTP reason. aiohttp uses *default reason + string* if not specified. + + :param str text: an optional text used in response body. If not specified *default + text* is constructed from status code and reason, e.g. `"404: Not + Found"`. + + :param str content_type: an optional Content-Type, `"text/plain"` by default. + + .. attribute:: status + + HTTP status code for the exception, :class:`int` + + .. attribute:: reason + + HTTP status reason for the exception, :class:`str` + + .. attribute:: text + + HTTP status reason for the exception, :class:`str` or ``None`` + for HTTP exceptions without body, e.g. "204 No Content" + + .. attribute:: headers + + HTTP headers for the exception, :class:`multidict.CIMultiDict` + + +Successful Exceptions +--------------------- + +HTTP exceptions for status code in range 200-299. They are not *errors* but special +classes reflected in exceptions hierarchy. E.g. ``raise web.HTTPNoContent`` may look +strange a little but the construction is absolutely legal. + +.. exception:: HTTPSuccessful + + A base class for the category, a subclass of :exc:`HTTPException`. + +.. exception:: HTTPOk + + An exception for *200 OK*, a subclass of :exc:`HTTPSuccessful`. + +.. exception:: HTTPCreated + + An exception for *201 Created*, a subclass of :exc:`HTTPSuccessful`. + +.. exception:: HTTPAccepted + + An exception for *202 Accepted*, a subclass of :exc:`HTTPSuccessful`. + +.. exception:: HTTPNonAuthoritativeInformation + + An exception for *203 Non-Authoritative Information*, a subclass of + :exc:`HTTPSuccessful`. + +.. exception:: HTTPNoContent + + An exception for *204 No Content*, a subclass of :exc:`HTTPSuccessful`. + + Has no HTTP body. + +.. exception:: HTTPResetContent + + An exception for *205 Reset Content*, a subclass of :exc:`HTTPSuccessful`. + + Has no HTTP body. + +.. exception:: HTTPPartialContent + + An exception for *206 Partial Content*, a subclass of :exc:`HTTPSuccessful`. + +Redirections +------------ + +HTTP exceptions for status code in range 300-399, e.g. ``raise +web.HTTPMovedPermanently(location='/new/path')``. + +.. exception:: HTTPRedirection + + A base class for the category, a subclass of :exc:`HTTPException`. + +.. exception:: HTTPMove(location, *, headers=None, reason=None, text=None, \ + content_type=None) + + A base class for redirections with implied *Location* header, + all redirections except :exc:`HTTPNotModified`. + + :param location: a :class:`yarl.URL` or :class:`str` used for *Location* HTTP + header. + + For other arguments see :exc:`HTTPException` constructor. + + .. attribute:: location + + A *Location* HTTP header value, :class:`yarl.URL`. + +.. exception:: HTTPMultipleChoices + + An exception for *300 Multiple Choices*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPMovedPermanently + + An exception for *301 Moved Permanently*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPFound + + An exception for *302 Found*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPSeeOther + + An exception for *303 See Other*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPNotModified + + An exception for *304 Not Modified*, a subclass of :exc:`HTTPRedirection`. + + Has no HTTP body. + +.. exception:: HTTPUseProxy + + An exception for *305 Use Proxy*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPTemporaryRedirect + + An exception for *307 Temporary Redirect*, a subclass of :exc:`HTTPMove`. + +.. exception:: HTTPPermanentRedirect + + An exception for *308 Permanent Redirect*, a subclass of :exc:`HTTPMove`. + + +Client Errors +------------- + +HTTP exceptions for status code in range 400-499, e.g. ``raise web.HTTPNotFound()``. + +.. exception:: HTTPClientError + + A base class for the category, a subclass of :exc:`HTTPException`. + +.. exception:: HTTPBadRequest + + An exception for *400 Bad Request*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPUnauthorized + + An exception for *401 Unauthorized*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPPaymentRequired + + An exception for *402 Payment Required*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPForbidden + + An exception for *403 Forbidden*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPNotFound + + An exception for *404 Not Found*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPMethodNotAllowed(method, allowed_methods, *, \ + headers=None, reason=None, text=None, \ + content_type=None) + + An exception for *405 Method Not Allowed*, a subclass of + :exc:`HTTPClientError`. + + :param str method: requested but not allowed HTTP method. + + :param allowed_methods: an iterable of allowed HTTP methods (:class:`str`), + *Allow* HTTP header is constructed from + the sequence separated by comma. + + For other arguments see :exc:`HTTPException` constructor. + + .. attribute:: allowed_methods + + A set of allowed HTTP methods. + + .. attribute:: method + + Requested but not allowed HTTP method. + +.. exception:: HTTPNotAcceptable + + An exception for *406 Not Acceptable*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPProxyAuthenticationRequired + + An exception for *407 Proxy Authentication Required*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPRequestTimeout + + An exception for *408 Request Timeout*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPConflict + + An exception for *409 Conflict*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPGone + + An exception for *410 Gone*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPLengthRequired + + An exception for *411 Length Required*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPPreconditionFailed + + An exception for *412 Precondition Failed*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPRequestEntityTooLarge(max_size, actual_size, **kwargs) + + An exception for *413 Entity Too Large*, a subclass of :exc:`HTTPClientError`. + + :param int max_size: Maximum allowed request body size + + :param int actual_size: Actual received size + + For other acceptable parameters see :exc:`HTTPException` constructor. + +.. exception:: HTTPRequestURITooLong + + An exception for *414 URI is too long*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPUnsupportedMediaType + + An exception for *415 Entity body in unsupported format*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPRequestRangeNotSatisfiable + + An exception for *416 Cannot satisfy request range*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPExpectationFailed + + An exception for *417 Expect condition could not be satisfied*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPMisdirectedRequest + + An exception for *421 Misdirected Request*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPUnprocessableEntity + + An exception for *422 Unprocessable Entity*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPFailedDependency + + An exception for *424 Failed Dependency*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPUpgradeRequired + + An exception for *426 Upgrade Required*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPPreconditionRequired + + An exception for *428 Precondition Required*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPTooManyRequests + + An exception for *429 Too Many Requests*, a subclass of :exc:`HTTPClientError`. + +.. exception:: HTTPRequestHeaderFieldsTooLarge + + An exception for *431 Requests Header Fields Too Large*, a subclass of + :exc:`HTTPClientError`. + +.. exception:: HTTPUnavailableForLegalReasons(link, *, \ + headers=None, \ + reason=None, \ + text=None, \ + content_type=None) + + + An exception for *451 Unavailable For Legal Reasons*, a subclass of + :exc:`HTTPClientError`. + + :param link: A link to a resource with information for blocking reason, + :class:`str` or :class:`URL` + + For other parameters see :exc:`HTTPException` constructor. + + .. attribute:: link + + A :class:`URL` link to a resource with information for blocking reason, + read-only property. + + +Server Errors +------------- + +HTTP exceptions for status code in range 500-599, e.g. ``raise web.HTTPBadGateway()``. + + +.. exception:: HTTPServerError + + A base class for the category, a subclass of :exc:`HTTPException`. + +.. exception:: HTTPInternalServerError + + An exception for *500 Server got itself in trouble*, a subclass of + :exc:`HTTPServerError`. + +.. exception:: HTTPNotImplemented + + An exception for *501 Server does not support this operation*, a subclass of + :exc:`HTTPServerError`. + +.. exception:: HTTPBadGateway + + An exception for *502 Invalid responses from another server/proxy*, a + subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPServiceUnavailable + + An exception for *503 The server cannot process the request due to a high + load*, a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPGatewayTimeout + + An exception for *504 The gateway server did not receive a timely response*, + a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPVersionNotSupported + + An exception for *505 Cannot fulfill request*, a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPVariantAlsoNegotiates + + An exception for *506 Variant Also Negotiates*, a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPInsufficientStorage + + An exception for *507 Insufficient Storage*, a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPNotExtended + + An exception for *510 Not Extended*, a subclass of :exc:`HTTPServerError`. + +.. exception:: HTTPNetworkAuthenticationRequired + + An exception for *511 Network Authentication Required*, a subclass of + :exc:`HTTPServerError`. diff --git a/docs/web_quickstart.rst b/docs/web_quickstart.rst index 0026ebbc293..a11a31c00a8 100644 --- a/docs/web_quickstart.rst +++ b/docs/web_quickstart.rst @@ -650,110 +650,3 @@ Example with login validation:: app.router.add_get('/', index, name='index') app.router.add_get('/login', login, name='login') app.router.add_post('/login', login, name='login') - -.. _aiohttp-web-exceptions: - -Exceptions ----------- - -:mod:`aiohttp.web` defines a set of exceptions for every *HTTP status code*. - -Each exception is a subclass of :class:`~HTTPException` and relates to a single -HTTP status code:: - - async def handler(request): - raise aiohttp.web.HTTPFound('/redirect') - -.. warning:: - - Returning :class:`~HTTPException` or its subclasses is deprecated and will - be removed in subsequent aiohttp versions. - -Each exception class has a status code according to :rfc:`2068`: -codes with 100-300 are not really errors; 400s are client errors, -and 500s are server errors. - -HTTP Exception hierarchy chart:: - - Exception - HTTPException - HTTPSuccessful - * 200 - HTTPOk - * 201 - HTTPCreated - * 202 - HTTPAccepted - * 203 - HTTPNonAuthoritativeInformation - * 204 - HTTPNoContent - * 205 - HTTPResetContent - * 206 - HTTPPartialContent - HTTPRedirection - * 300 - HTTPMultipleChoices - * 301 - HTTPMovedPermanently - * 302 - HTTPFound - * 303 - HTTPSeeOther - * 304 - HTTPNotModified - * 305 - HTTPUseProxy - * 307 - HTTPTemporaryRedirect - * 308 - HTTPPermanentRedirect - HTTPError - HTTPClientError - * 400 - HTTPBadRequest - * 401 - HTTPUnauthorized - * 402 - HTTPPaymentRequired - * 403 - HTTPForbidden - * 404 - HTTPNotFound - * 405 - HTTPMethodNotAllowed - * 406 - HTTPNotAcceptable - * 407 - HTTPProxyAuthenticationRequired - * 408 - HTTPRequestTimeout - * 409 - HTTPConflict - * 410 - HTTPGone - * 411 - HTTPLengthRequired - * 412 - HTTPPreconditionFailed - * 413 - HTTPRequestEntityTooLarge - * 414 - HTTPRequestURITooLong - * 415 - HTTPUnsupportedMediaType - * 416 - HTTPRequestRangeNotSatisfiable - * 417 - HTTPExpectationFailed - * 421 - HTTPMisdirectedRequest - * 422 - HTTPUnprocessableEntity - * 424 - HTTPFailedDependency - * 426 - HTTPUpgradeRequired - * 428 - HTTPPreconditionRequired - * 429 - HTTPTooManyRequests - * 431 - HTTPRequestHeaderFieldsTooLarge - * 451 - HTTPUnavailableForLegalReasons - HTTPServerError - * 500 - HTTPInternalServerError - * 501 - HTTPNotImplemented - * 502 - HTTPBadGateway - * 503 - HTTPServiceUnavailable - * 504 - HTTPGatewayTimeout - * 505 - HTTPVersionNotSupported - * 506 - HTTPVariantAlsoNegotiates - * 507 - HTTPInsufficientStorage - * 510 - HTTPNotExtended - * 511 - HTTPNetworkAuthenticationRequired - -All HTTP exceptions have the same constructor signature:: - - HTTPNotFound(*, headers=None, reason=None, - body=None, text=None, content_type=None) - -If not directly specified, *headers* will be added to the *default -response headers*. - -Classes :class:`HTTPMultipleChoices`, :class:`HTTPMovedPermanently`, -:class:`HTTPFound`, :class:`HTTPSeeOther`, :class:`HTTPUseProxy`, -:class:`HTTPTemporaryRedirect` have the following constructor signature:: - - HTTPFound(location, *, headers=None, reason=None, - body=None, text=None, content_type=None) - -where *location* is value for *Location HTTP header*. - -:class:`HTTPMethodNotAllowed` is constructed by providing the incoming -unsupported method and list of allowed methods:: - - HTTPMethodNotAllowed(method, allowed_methods, *, - headers=None, reason=None, - body=None, text=None, content_type=None) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 3c6461b8e01..c8e82ca452f 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -1218,40 +1218,6 @@ Return :class:`Response` with predefined ``'application/json'`` content type and *data* encoded by ``dumps`` parameter (:func:`json.dumps` by default). -HTTP Exceptions -^^^^^^^^^^^^^^^ -Errors can also be returned by raising a HTTP exception instance from within -the handler. - -.. class:: HTTPException(*, headers=None, reason=None, text=None, content_type=None) - - Low-level HTTP failure. - - :param headers: headers for the response - :type headers: dict or multidict.CIMultiDict - - :param str reason: reason included in the response - - :param str text: response's body - - :param str content_type: response's content type. This is passed through - to the :class:`Response` initializer. - - Sub-classes of ``HTTPException`` exist for the standard HTTP response codes - as described in :ref:`aiohttp-web-exceptions` and the expected usage is to - simply raise the appropriate exception type to respond with a specific HTTP - response code. - - Since ``HTTPException`` is a sub-class of :class:`Response`, it contains the - methods and properties that allow you to directly manipulate details of the - response. - - .. attribute:: status_code - - HTTP status code for this exception class. This attribute is usually - defined at the class level. ``self.status_code`` is passed to the - :class:`Response` initializer. - .. _aiohttp-web-app-and-router: diff --git a/tests/test_web_exceptions.py b/tests/test_web_exceptions.py index e82a907a8d9..cebb1f1362f 100644 --- a/tests/test_web_exceptions.py +++ b/tests/test_web_exceptions.py @@ -1,47 +1,10 @@ import collections -import re from traceback import format_exception -from unittest import mock import pytest +from yarl import URL -from aiohttp import helpers, signals, web -from aiohttp.test_utils import make_mocked_request - - -@pytest.fixture -def buf(): - return bytearray() - - -@pytest.fixture -def http_request(buf): - method = 'GET' - path = '/' - writer = mock.Mock() - writer.drain.return_value = () - - def append(data=b''): - buf.extend(data) - return helpers.noop() - - async def write_headers(status_line, headers): - headers = status_line + '\r\n' + ''.join( - [k + ': ' + v + '\r\n' for k, v in headers.items()]) - headers = headers.encode('utf-8') + b'\r\n' - buf.extend(headers) - - writer.buffer_data.side_effect = append - writer.write.side_effect = append - writer.write_eof.side_effect = append - writer.write_headers.side_effect = write_headers - - app = mock.Mock() - app._debug = False - app.on_response_prepare = signals.Signal(app) - app.on_response_prepare.freeze() - req = make_mocked_request(method, path, app=app, writer=writer) - return req +from aiohttp import web def test_all_http_exceptions_exported() -> None: @@ -54,17 +17,49 @@ def test_all_http_exceptions_exported() -> None: assert name in web.__all__ -async def test_HTTPOk(buf, http_request) -> None: +async def test_ctor() -> None: resp = web.HTTPOk() - await resp.prepare(http_request) - await resp.write_eof() - txt = buf.decode('utf8') - assert re.match(('HTTP/1.1 200 OK\r\n' - 'Content-Type: text/plain; charset=utf-8\r\n' - 'Content-Length: 7\r\n' - 'Date: .+\r\n' - 'Server: .+\r\n\r\n' - '200: OK'), txt) + assert resp.text == "200: OK" + assert resp.headers == {'Content-Type': 'text/plain'} + assert resp.reason == "OK" + assert resp.status == 200 + assert bool(resp) + + +async def test_ctor_with_headers() -> None: + resp = web.HTTPOk(headers={"X-Custom": "value"}) + assert resp.text == "200: OK" + assert resp.headers == {'Content-Type': 'text/plain', "X-Custom": "value"} + assert resp.reason == "OK" + assert resp.status == 200 + + +async def test_ctor_content_type() -> None: + resp = web.HTTPOk(text="text", content_type="custom") + assert resp.text == "text" + assert resp.headers == {'Content-Type': 'custom'} + assert resp.reason == "OK" + assert resp.status == 200 + assert bool(resp) + + +async def test_ctor_content_type_without_text() -> None: + with pytest.warns(DeprecationWarning): + resp = web.HTTPResetContent(content_type="custom") + assert resp.text is None + assert resp.headers == {'Content-Type': 'custom'} + assert resp.reason == "Reset Content" + assert resp.status == 205 + assert bool(resp) + + +async def test_ctor_text_for_empty_body() -> None: + with pytest.warns(DeprecationWarning): + resp = web.HTTPResetContent(text="text") + assert resp.text == "text" + assert resp.headers == {'Content-Type': 'text/plain'} + assert resp.reason == "Reset Content" + assert resp.status == 205 def test_terminal_classes_has_status_code() -> None: @@ -87,20 +82,47 @@ def test_terminal_classes_has_status_code() -> None: assert 1 == codes.most_common(1)[0][1] -async def test_HTTPFound(buf, http_request) -> None: - resp = web.HTTPFound(location='/redirect') - assert '/redirect' == resp.location +async def test_HTTPOk(aiohttp_client) -> None: + + async def handler(request): + raise web.HTTPOk() + + app = web.Application() + app.router.add_get('/', handler) + cli = await aiohttp_client(app) + + resp = await cli.get('/') + assert 200 == resp.status + txt = await resp.text() + assert "200: OK" == txt + + +async def test_HTTPFound(aiohttp_client) -> None: + + async def handler(request): + raise web.HTTPFound(location='/redirect') + + app = web.Application() + app.router.add_get('/', handler) + cli = await aiohttp_client(app) + + resp = await cli.get('/', allow_redirects=False) + assert 302 == resp.status + txt = await resp.text() + assert "302: Found" == txt assert '/redirect' == resp.headers['location'] - await resp.prepare(http_request) - await resp.write_eof() - txt = buf.decode('utf8') - assert re.match('HTTP/1.1 302 Found\r\n' - 'Content-Type: text/plain; charset=utf-8\r\n' - 'Location: /redirect\r\n' - 'Content-Length: 10\r\n' - 'Date: .+\r\n' - 'Server: .+\r\n\r\n' - '302: Found', txt) + + +def test_HTTPFound_location_str() -> None: + exc = web.HTTPFound(location='/redirect') + assert exc.location == URL('/redirect') + assert exc.headers['Location'] == '/redirect' + + +def test_HTTPFound_location_url() -> None: + exc = web.HTTPFound(location=URL('/redirect')) + assert exc.location == URL('/redirect') + assert exc.headers['Location'] == '/redirect' def test_HTTPFound_empty_location() -> None: @@ -111,68 +133,45 @@ def test_HTTPFound_empty_location() -> None: web.HTTPFound(location=None) -async def test_HTTPMethodNotAllowed(buf, http_request) -> None: - resp = web.HTTPMethodNotAllowed('get', ['POST', 'PUT']) - assert 'GET' == resp.method - assert {'POST', 'PUT'} == resp.allowed_methods - assert 'POST,PUT' == resp.headers['allow'] - await resp.prepare(http_request) - await resp.write_eof() - txt = buf.decode('utf8') - assert re.match('HTTP/1.1 405 Method Not Allowed\r\n' - 'Content-Type: text/plain; charset=utf-8\r\n' - 'Allow: POST,PUT\r\n' - 'Content-Length: 23\r\n' - 'Date: .+\r\n' - 'Server: .+\r\n\r\n' - '405: Method Not Allowed', txt) - - -def test_override_body_with_text() -> None: - resp = web.HTTPNotFound(text="Page not found") - assert 404 == resp.status - assert "Page not found".encode('utf-8') == resp.body - assert "Page not found" == resp.text - assert "text/plain" == resp.content_type - assert "utf-8" == resp.charset +async def test_HTTPMethodNotAllowed() -> None: + exc = web.HTTPMethodNotAllowed('GET', ['POST', 'PUT']) + assert 'GET' == exc.method + assert {'POST', 'PUT'} == exc.allowed_methods + assert 'POST,PUT' == exc.headers['allow'] + assert '405: Method Not Allowed' == exc.text -def test_override_body_with_binary() -> None: - txt = "Page not found" - with pytest.warns(DeprecationWarning): - resp = web.HTTPNotFound(body=txt.encode('utf-8'), - content_type="text/html") +def test_with_text() -> None: + resp = web.HTTPNotFound(text="Page not found") assert 404 == resp.status - assert txt.encode('utf-8') == resp.body - assert txt == resp.text - assert "text/html" == resp.content_type - assert resp.charset is None + assert "Page not found" == resp.text + assert "text/plain" == resp.headers['Content-Type'] -def test_default_body() -> None: +def test_default_text() -> None: resp = web.HTTPOk() - assert b'200: OK' == resp.body + assert '200: OK' == resp.text -def test_empty_body_204() -> None: +def test_empty_text_204() -> None: resp = web.HTTPNoContent() - assert resp.body is None + assert resp.text is None -def test_empty_body_205() -> None: +def test_empty_text_205() -> None: resp = web.HTTPNoContent() - assert resp.body is None + assert resp.text is None -def test_empty_body_304() -> None: +def test_empty_text_304() -> None: resp = web.HTTPNoContent() - resp.body is None + resp.text is None -def test_link_header_451(buf) -> None: +def test_link_header_451() -> None: resp = web.HTTPUnavailableForLegalReasons(link='http://warning.or.kr/') - assert 'http://warning.or.kr/' == resp.link + assert URL('http://warning.or.kr/') == resp.link assert '; rel="blocked-by"' == resp.headers['Link'] diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 384536c5285..6f7a2f250a9 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -1868,17 +1868,17 @@ async def resolve(self, host, port=0, family=socket.AF_INET): await client.close() -async def test_return_http_exception_deprecated(aiohttp_client) -> None: +async def test_raise_http_exception(aiohttp_client) -> None: async def handler(request): - return web.HTTPForbidden() + raise web.HTTPForbidden() app = web.Application() app.router.add_route('GET', '/', handler) client = await aiohttp_client(app) - with pytest.warns(DeprecationWarning): - await client.get('/') + resp = await client.get('/') + assert resp.status == 403 async def test_request_path(aiohttp_client) -> None: