src/genro_asgi/exceptions.py¶
Source from this local checkout, regenerated when the reader rebuilds.
Line links use #L<number>; a GitHub line range opens its first line.
1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 # https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """HTTP control-flow exceptions: raised anywhere, answered by the errors16 middleware.17 18 Plain classes, no framework machinery: ``HTTPException(status, detail=None,19 headers=None)`` carries the response status (an optional plain-text detail and20 optional response ``headers`` — ASGI ``(name, value)`` byte pairs forwarded to21 the response, e.g. a ``WWW-Authenticate`` challenge on a 401); the common22 errors are pre-filled subclasses — ``HTTPBadRequest`` (400), ``HTTPNotFound``23 (404), ``HTTPUnauthorized`` (401), ``HTTPForbidden`` (403),24 ``HTTPUnprocessableContent`` (422). ``Redirect(location, status=302)`` is the25 redirecting sibling: its ``location`` becomes the ``Location`` header. The26 mapping to actual ASGI responses lives in ``middleware/errors.py``.27 28 One exception here is not an HTTP error at all: ``WebSocketDisconnect`` is how29 the websocket facade reports that the client is gone. A disconnect is not a30 value a read can return — every read would have to be checked — so it arrives31 as an exception, and the read loop that catches it simply ends.32 """33 34 from __future__ import annotations35 36 __all__ = [37 "HTTPBadRequest",38 "HTTPException",39 "HTTPForbidden",40 "HTTPNotFound",41 "HTTPUnauthorized",42 "HTTPUnprocessableContent",43 "Redirect",44 "WebSocketDisconnect",45 ]46 47 48 class HTTPException(Exception):49 """HTTP error carried as an exception: ``status``, optional ``detail`` and ``headers``.50 51 ``headers`` are ASGI ``(name, value)`` byte pairs the errors middleware52 forwards onto the response (e.g. a ``WWW-Authenticate`` challenge).53 """54 55 def __init__(56 self,57 status: int,58 detail: str | None = None,59 headers: list[tuple[bytes, bytes]] | None = None,60 ) -> None:61 super().__init__(detail if detail is not None else f"HTTP {status}")62 self.status = status63 self.detail = detail64 self.headers: list[tuple[bytes, bytes]] = headers or []65 66 67 class HTTPBadRequest(HTTPException):68 """400 Bad Request."""69 70 def __init__(71 self, detail: str | None = None, headers: list[tuple[bytes, bytes]] | None = None72 ) -> None:73 super().__init__(400, detail, headers)74 75 76 class HTTPNotFound(HTTPException):77 """404 Not Found."""78 79 def __init__(80 self, detail: str | None = None, headers: list[tuple[bytes, bytes]] | None = None81 ) -> None:82 super().__init__(404, detail, headers)83 84 85 class HTTPUnauthorized(HTTPException):86 """401 Unauthorized."""87 88 def __init__(89 self, detail: str | None = None, headers: list[tuple[bytes, bytes]] | None = None90 ) -> None:91 super().__init__(401, detail, headers)92 93 94 class HTTPForbidden(HTTPException):95 """403 Forbidden."""96 97 def __init__(98 self, detail: str | None = None, headers: list[tuple[bytes, bytes]] | None = None99 ) -> None:100 super().__init__(403, detail, headers)101 102 103 class HTTPUnprocessableContent(HTTPException):104 """422 Unprocessable Content."""105 106 def __init__(107 self, detail: str | None = None, headers: list[tuple[bytes, bytes]] | None = None108 ) -> None:109 super().__init__(422, detail, headers)110 111 112 class Redirect(HTTPException):113 """HTTP redirect: ``location`` becomes the ``Location`` header."""114 115 def __init__(116 self, location: str, status: int = 302, headers: list[tuple[bytes, bytes]] | None = None117 ) -> None:118 super().__init__(status, headers=headers)119 self.location = location120 121 122 class WebSocketDisconnect(Exception):123 """The client is gone: ``code`` and ``reason`` as the ASGI message carried them.124 125 Raised by every read of the facade, and by its iterator, which ends on it.126 The default is 1000 with no reason, which is what an ASGI server sends when127 the disconnect message carries neither.128 """129 130 def __init__(self, code: int = 1000, reason: str = "") -> None:131 super().__init__(f"websocket disconnected: {code} {reason}".rstrip())132 self.code = code133 self.reason = reason