1"""Implements a number of Python exceptions which can be raised from within
2a view to trigger a standard HTTP non-200 response.
3
4Usage Example
5-------------
6
7.. code-block:: python
8
9 from werkzeug.wrappers.request import Request
10 from werkzeug.exceptions import HTTPException, NotFound
11
12 def view(request):
13 raise NotFound()
14
15 @Request.application
16 def application(request):
17 try:
18 return view(request)
19 except HTTPException as e:
20 return e
21
22As you can see from this example those exceptions are callable WSGI
23applications. However, they are not Werkzeug response objects. You
24can get a response object by calling ``get_response()`` on a HTTP
25exception.
26
27Keep in mind that you may have to pass an environ (WSGI) or scope
28(ASGI) to ``get_response()`` because some errors fetch additional
29information relating to the request.
30
31If you want to hook in a different exception page to say, a 404 status
32code, you can add a second except for a specific subclass of an error:
33
34.. code-block:: python
35
36 @Request.application
37 def application(request):
38 try:
39 return view(request)
40 except NotFound as e:
41 return not_found(request)
42 except HTTPException as e:
43 return e
44
45"""
46
47from __future__ import annotations
48
49import typing as t
50from datetime import datetime
51
52from markupsafe import escape
53from markupsafe import Markup
54
55from ._internal import _get_environ
56
57if t.TYPE_CHECKING:
58 from _typeshed.wsgi import StartResponse
59 from _typeshed.wsgi import WSGIEnvironment
60
61 from .datastructures import WWWAuthenticate
62 from .sansio.response import Response
63 from .wrappers.request import Request as WSGIRequest
64 from .wrappers.response import Response as WSGIResponse
65
66
67class HTTPException(Exception):
68 """The base class for all HTTP exceptions. This exception can be called as a WSGI
69 application to render a default error page or you can catch the subclasses
70 of it independently and render nicer error messages.
71
72 .. versionchanged:: 2.1
73 Removed the ``wrap`` class method.
74 """
75
76 code: int | None = None
77 description: str | None = None
78
79 def __init__(
80 self,
81 description: str | None = None,
82 response: Response | None = None,
83 ) -> None:
84 super().__init__()
85 if description is not None:
86 self.description = description
87 self.response = response
88
89 @property
90 def name(self) -> str:
91 """The status name."""
92 from .http import HTTP_STATUS_CODES
93
94 return HTTP_STATUS_CODES.get(self.code, "Unknown Error") # type: ignore
95
96 def get_description(
97 self,
98 environ: WSGIEnvironment | None = None,
99 scope: dict[str, t.Any] | None = None,
100 ) -> str:
101 """Get the description."""
102 if self.description is None:
103 description = ""
104 else:
105 description = self.description
106
107 description = escape(description).replace("\n", Markup("<br>"))
108 return f"<p>{description}</p>"
109
110 def get_body(
111 self,
112 environ: WSGIEnvironment | None = None,
113 scope: dict[str, t.Any] | None = None,
114 ) -> str:
115 """Get the HTML body."""
116 return (
117 "<!doctype html>\n"
118 "<html lang=en>\n"
119 f"<title>{self.code} {escape(self.name)}</title>\n"
120 f"<h1>{escape(self.name)}</h1>\n"
121 f"{self.get_description(environ)}\n"
122 )
123
124 def get_headers(
125 self,
126 environ: WSGIEnvironment | None = None,
127 scope: dict[str, t.Any] | None = None,
128 ) -> list[tuple[str, str]]:
129 """Get a list of headers."""
130 return [("Content-Type", "text/html; charset=utf-8")]
131
132 def get_response(
133 self,
134 environ: WSGIEnvironment | WSGIRequest | None = None,
135 scope: dict[str, t.Any] | None = None,
136 ) -> Response:
137 """Get a response object. If one was passed to the exception
138 it's returned directly.
139
140 :param environ: the optional environ for the request. This
141 can be used to modify the response depending
142 on how the request looked like.
143 :return: a :class:`Response` object or a subclass thereof.
144 """
145 from .wrappers.response import Response as WSGIResponse # noqa: F811
146
147 if self.response is not None:
148 return self.response
149 if environ is not None:
150 environ = _get_environ(environ)
151 headers = self.get_headers(environ, scope)
152 return WSGIResponse(self.get_body(environ, scope), self.code, headers)
153
154 def __call__(
155 self, environ: WSGIEnvironment, start_response: StartResponse
156 ) -> t.Iterable[bytes]:
157 """Call the exception as WSGI application.
158
159 :param environ: the WSGI environment.
160 :param start_response: the response callable provided by the WSGI
161 server.
162 """
163 response = t.cast("WSGIResponse", self.get_response(environ))
164 return response(environ, start_response)
165
166 def __str__(self) -> str:
167 code = self.code if self.code is not None else "???"
168 return f"{code} {self.name}: {self.description}"
169
170 def __repr__(self) -> str:
171 code = self.code if self.code is not None else "???"
172 return f"<{type(self).__name__} '{code}: {self.name}'>"
173
174
175class BadRequest(HTTPException):
176 """*400* `Bad Request`
177
178 Raise if the browser sends something to the application the application
179 or server cannot handle.
180 """
181
182 code = 400
183 description = (
184 "The browser (or proxy) sent a request that this server could "
185 "not understand."
186 )
187
188
189class BadRequestKeyError(BadRequest, KeyError):
190 """An exception that is used to signal both a :exc:`KeyError` and a
191 :exc:`BadRequest`. Used by many of the datastructures.
192 """
193
194 _description = BadRequest.description
195 #: Show the KeyError along with the HTTP error message in the
196 #: response. This should be disabled in production, but can be
197 #: useful in a debug mode.
198 show_exception = False
199
200 def __init__(self, arg: str | None = None, *args: t.Any, **kwargs: t.Any):
201 super().__init__(*args, **kwargs)
202
203 if arg is None:
204 KeyError.__init__(self)
205 else:
206 KeyError.__init__(self, arg)
207
208 @property # type: ignore
209 def description(self) -> str:
210 if self.show_exception:
211 return (
212 f"{self._description}\n"
213 f"{KeyError.__name__}: {KeyError.__str__(self)}"
214 )
215
216 return self._description
217
218 @description.setter
219 def description(self, value: str) -> None:
220 self._description = value
221
222
223class ClientDisconnected(BadRequest):
224 """Internal exception that is raised if Werkzeug detects a disconnected
225 client. Since the client is already gone at that point attempting to
226 send the error message to the client might not work and might ultimately
227 result in another exception in the server. Mainly this is here so that
228 it is silenced by default as far as Werkzeug is concerned.
229
230 Since disconnections cannot be reliably detected and are unspecified
231 by WSGI to a large extent this might or might not be raised if a client
232 is gone.
233
234 .. versionadded:: 0.8
235 """
236
237
238class SecurityError(BadRequest):
239 """Raised if something triggers a security error. This is otherwise
240 exactly like a bad request error.
241
242 .. versionadded:: 0.9
243 """
244
245
246class BadHost(BadRequest):
247 """Raised if the submitted host is badly formatted.
248
249 .. versionadded:: 0.11.2
250 """
251
252
253class Unauthorized(HTTPException):
254 """*401* ``Unauthorized``
255
256 Raise if the user is not authorized to access a resource.
257
258 The ``www_authenticate`` argument should be used to set the
259 ``WWW-Authenticate`` header. This is used for HTTP basic auth and
260 other schemes. Use :class:`~werkzeug.datastructures.WWWAuthenticate`
261 to create correctly formatted values. Strictly speaking a 401
262 response is invalid if it doesn't provide at least one value for
263 this header, although real clients typically don't care.
264
265 :param description: Override the default message used for the body
266 of the response.
267 :param www-authenticate: A single value, or list of values, for the
268 WWW-Authenticate header(s).
269
270 .. versionchanged:: 2.0
271 Serialize multiple ``www_authenticate`` items into multiple
272 ``WWW-Authenticate`` headers, rather than joining them
273 into a single value, for better interoperability.
274
275 .. versionchanged:: 0.15.3
276 If the ``www_authenticate`` argument is not set, the
277 ``WWW-Authenticate`` header is not set.
278
279 .. versionchanged:: 0.15.3
280 The ``response`` argument was restored.
281
282 .. versionchanged:: 0.15.1
283 ``description`` was moved back as the first argument, restoring
284 its previous position.
285
286 .. versionchanged:: 0.15.0
287 ``www_authenticate`` was added as the first argument, ahead of
288 ``description``.
289 """
290
291 code = 401
292 description = (
293 "The server could not verify that you are authorized to access"
294 " the URL requested. You either supplied the wrong credentials"
295 " (e.g. a bad password), or your browser doesn't understand"
296 " how to supply the credentials required."
297 )
298
299 def __init__(
300 self,
301 description: str | None = None,
302 response: Response | None = None,
303 www_authenticate: None | (WWWAuthenticate | t.Iterable[WWWAuthenticate]) = None,
304 ) -> None:
305 super().__init__(description, response)
306
307 from .datastructures import WWWAuthenticate
308
309 if isinstance(www_authenticate, WWWAuthenticate):
310 www_authenticate = (www_authenticate,)
311
312 self.www_authenticate = www_authenticate
313
314 def get_headers(
315 self,
316 environ: WSGIEnvironment | None = None,
317 scope: dict[str, t.Any] | None = None,
318 ) -> list[tuple[str, str]]:
319 headers = super().get_headers(environ, scope)
320 if self.www_authenticate:
321 headers.extend(("WWW-Authenticate", str(x)) for x in self.www_authenticate)
322 return headers
323
324
325class Forbidden(HTTPException):
326 """*403* `Forbidden`
327
328 Raise if the user doesn't have the permission for the requested resource
329 but was authenticated.
330 """
331
332 code = 403
333 description = (
334 "You don't have the permission to access the requested"
335 " resource. It is either read-protected or not readable by the"
336 " server."
337 )
338
339
340class NotFound(HTTPException):
341 """*404* `Not Found`
342
343 Raise if a resource does not exist and never existed.
344 """
345
346 code = 404
347 description = (
348 "The requested URL was not found on the server. If you entered"
349 " the URL manually please check your spelling and try again."
350 )
351
352
353class MethodNotAllowed(HTTPException):
354 """*405* `Method Not Allowed`
355
356 Raise if the server used a method the resource does not handle. For
357 example `POST` if the resource is view only. Especially useful for REST.
358
359 The first argument for this exception should be a list of allowed methods.
360 Strictly speaking the response would be invalid if you don't provide valid
361 methods in the header which you can do with that list.
362 """
363
364 code = 405
365 description = "The method is not allowed for the requested URL."
366
367 def __init__(
368 self,
369 valid_methods: t.Iterable[str] | None = None,
370 description: str | None = None,
371 response: Response | None = None,
372 ) -> None:
373 """Takes an optional list of valid http methods
374 starting with werkzeug 0.3 the list will be mandatory."""
375 super().__init__(description=description, response=response)
376 self.valid_methods = valid_methods
377
378 def get_headers(
379 self,
380 environ: WSGIEnvironment | None = None,
381 scope: dict[str, t.Any] | None = None,
382 ) -> list[tuple[str, str]]:
383 headers = super().get_headers(environ, scope)
384 if self.valid_methods:
385 headers.append(("Allow", ", ".join(self.valid_methods)))
386 return headers
387
388
389class NotAcceptable(HTTPException):
390 """*406* `Not Acceptable`
391
392 Raise if the server can't return any content conforming to the
393 `Accept` headers of the client.
394 """
395
396 code = 406
397 description = (
398 "The resource identified by the request is only capable of"
399 " generating response entities which have content"
400 " characteristics not acceptable according to the accept"
401 " headers sent in the request."
402 )
403
404
405class RequestTimeout(HTTPException):
406 """*408* `Request Timeout`
407
408 Raise to signalize a timeout.
409 """
410
411 code = 408
412 description = (
413 "The server closed the network connection because the browser"
414 " didn't finish the request within the specified time."
415 )
416
417
418class Conflict(HTTPException):
419 """*409* `Conflict`
420
421 Raise to signal that a request cannot be completed because it conflicts
422 with the current state on the server.
423
424 .. versionadded:: 0.7
425 """
426
427 code = 409
428 description = (
429 "A conflict happened while processing the request. The"
430 " resource might have been modified while the request was being"
431 " processed."
432 )
433
434
435class Gone(HTTPException):
436 """*410* `Gone`
437
438 Raise if a resource existed previously and went away without new location.
439 """
440
441 code = 410
442 description = (
443 "The requested URL is no longer available on this server and"
444 " there is no forwarding address. If you followed a link from a"
445 " foreign page, please contact the author of this page."
446 )
447
448
449class LengthRequired(HTTPException):
450 """*411* `Length Required`
451
452 Raise if the browser submitted data but no ``Content-Length`` header which
453 is required for the kind of processing the server does.
454 """
455
456 code = 411
457 description = (
458 "A request with this method requires a valid <code>Content-"
459 "Length</code> header."
460 )
461
462
463class PreconditionFailed(HTTPException):
464 """*412* `Precondition Failed`
465
466 Status code used in combination with ``If-Match``, ``If-None-Match``, or
467 ``If-Unmodified-Since``.
468 """
469
470 code = 412
471 description = (
472 "The precondition on the request for the URL failed positive evaluation."
473 )
474
475
476class RequestEntityTooLarge(HTTPException):
477 """*413* `Request Entity Too Large`
478
479 The status code one should return if the data submitted exceeded a given
480 limit.
481 """
482
483 code = 413
484 description = "The data value transmitted exceeds the capacity limit."
485
486
487class RequestURITooLarge(HTTPException):
488 """*414* `Request URI Too Large`
489
490 Like *413* but for too long URLs.
491 """
492
493 code = 414
494 description = (
495 "The length of the requested URL exceeds the capacity limit for"
496 " this server. The request cannot be processed."
497 )
498
499
500class UnsupportedMediaType(HTTPException):
501 """*415* `Unsupported Media Type`
502
503 The status code returned if the server is unable to handle the media type
504 the client transmitted.
505 """
506
507 code = 415
508 description = (
509 "The server does not support the media type transmitted in the request."
510 )
511
512
513class RequestedRangeNotSatisfiable(HTTPException):
514 """*416* `Requested Range Not Satisfiable`
515
516 The client asked for an invalid part of the file.
517
518 .. versionadded:: 0.7
519 """
520
521 code = 416
522 description = "The server cannot provide the requested range."
523
524 def __init__(
525 self,
526 length: int | None = None,
527 units: str = "bytes",
528 description: str | None = None,
529 response: Response | None = None,
530 ) -> None:
531 """Takes an optional `Content-Range` header value based on ``length``
532 parameter.
533 """
534 super().__init__(description=description, response=response)
535 self.length = length
536 self.units = units
537
538 def get_headers(
539 self,
540 environ: WSGIEnvironment | None = None,
541 scope: dict[str, t.Any] | None = None,
542 ) -> list[tuple[str, str]]:
543 headers = super().get_headers(environ, scope)
544 if self.length is not None:
545 headers.append(("Content-Range", f"{self.units} */{self.length}"))
546 return headers
547
548
549class ExpectationFailed(HTTPException):
550 """*417* `Expectation Failed`
551
552 The server cannot meet the requirements of the Expect request-header.
553
554 .. versionadded:: 0.7
555 """
556
557 code = 417
558 description = "The server could not meet the requirements of the Expect header"
559
560
561class ImATeapot(HTTPException):
562 """*418* `I'm a teapot`
563
564 The server should return this if it is a teapot and someone attempted
565 to brew coffee with it.
566
567 .. versionadded:: 0.7
568 """
569
570 code = 418
571 description = "This server is a teapot, not a coffee machine"
572
573
574class UnprocessableEntity(HTTPException):
575 """*422* `Unprocessable Entity`
576
577 Used if the request is well formed, but the instructions are otherwise
578 incorrect.
579 """
580
581 code = 422
582 description = (
583 "The request was well-formed but was unable to be followed due"
584 " to semantic errors."
585 )
586
587
588class Locked(HTTPException):
589 """*423* `Locked`
590
591 Used if the resource that is being accessed is locked.
592 """
593
594 code = 423
595 description = "The resource that is being accessed is locked."
596
597
598class FailedDependency(HTTPException):
599 """*424* `Failed Dependency`
600
601 Used if the method could not be performed on the resource
602 because the requested action depended on another action and that action failed.
603 """
604
605 code = 424
606 description = (
607 "The method could not be performed on the resource because the"
608 " requested action depended on another action and that action"
609 " failed."
610 )
611
612
613class PreconditionRequired(HTTPException):
614 """*428* `Precondition Required`
615
616 The server requires this request to be conditional, typically to prevent
617 the lost update problem, which is a race condition between two or more
618 clients attempting to update a resource through PUT or DELETE. By requiring
619 each client to include a conditional header ("If-Match" or "If-Unmodified-
620 Since") with the proper value retained from a recent GET request, the
621 server ensures that each client has at least seen the previous revision of
622 the resource.
623 """
624
625 code = 428
626 description = (
627 "This request is required to be conditional; try using"
628 ' "If-Match" or "If-Unmodified-Since".'
629 )
630
631
632class _RetryAfter(HTTPException):
633 """Adds an optional ``retry_after`` parameter which will set the
634 ``Retry-After`` header. May be an :class:`int` number of seconds or
635 a :class:`~datetime.datetime`.
636 """
637
638 def __init__(
639 self,
640 description: str | None = None,
641 response: Response | None = None,
642 retry_after: datetime | int | None = None,
643 ) -> None:
644 super().__init__(description, response)
645 self.retry_after = retry_after
646
647 def get_headers(
648 self,
649 environ: WSGIEnvironment | None = None,
650 scope: dict[str, t.Any] | None = None,
651 ) -> list[tuple[str, str]]:
652 headers = super().get_headers(environ, scope)
653
654 if self.retry_after:
655 if isinstance(self.retry_after, datetime):
656 from .http import http_date
657
658 value = http_date(self.retry_after)
659 else:
660 value = str(self.retry_after)
661
662 headers.append(("Retry-After", value))
663
664 return headers
665
666
667class TooManyRequests(_RetryAfter):
668 """*429* `Too Many Requests`
669
670 The server is limiting the rate at which this user receives
671 responses, and this request exceeds that rate. (The server may use
672 any convenient method to identify users and their request rates).
673 The server may include a "Retry-After" header to indicate how long
674 the user should wait before retrying.
675
676 :param retry_after: If given, set the ``Retry-After`` header to this
677 value. May be an :class:`int` number of seconds or a
678 :class:`~datetime.datetime`.
679
680 .. versionchanged:: 1.0
681 Added ``retry_after`` parameter.
682 """
683
684 code = 429
685 description = "This user has exceeded an allotted request count. Try again later."
686
687
688class RequestHeaderFieldsTooLarge(HTTPException):
689 """*431* `Request Header Fields Too Large`
690
691 The server refuses to process the request because the header fields are too
692 large. One or more individual fields may be too large, or the set of all
693 headers is too large.
694 """
695
696 code = 431
697 description = "One or more header fields exceeds the maximum size."
698
699
700class UnavailableForLegalReasons(HTTPException):
701 """*451* `Unavailable For Legal Reasons`
702
703 This status code indicates that the server is denying access to the
704 resource as a consequence of a legal demand.
705 """
706
707 code = 451
708 description = "Unavailable for legal reasons."
709
710
711class InternalServerError(HTTPException):
712 """*500* `Internal Server Error`
713
714 Raise if an internal server error occurred. This is a good fallback if an
715 unknown error occurred in the dispatcher.
716
717 .. versionchanged:: 1.0.0
718 Added the :attr:`original_exception` attribute.
719 """
720
721 code = 500
722 description = (
723 "The server encountered an internal error and was unable to"
724 " complete your request. Either the server is overloaded or"
725 " there is an error in the application."
726 )
727
728 def __init__(
729 self,
730 description: str | None = None,
731 response: Response | None = None,
732 original_exception: BaseException | None = None,
733 ) -> None:
734 #: The original exception that caused this 500 error. Can be
735 #: used by frameworks to provide context when handling
736 #: unexpected errors.
737 self.original_exception = original_exception
738 super().__init__(description=description, response=response)
739
740
741class NotImplemented(HTTPException):
742 """*501* `Not Implemented`
743
744 Raise if the application does not support the action requested by the
745 browser.
746 """
747
748 code = 501
749 description = "The server does not support the action requested by the browser."
750
751
752class BadGateway(HTTPException):
753 """*502* `Bad Gateway`
754
755 If you do proxying in your application you should return this status code
756 if you received an invalid response from the upstream server it accessed
757 in attempting to fulfill the request.
758 """
759
760 code = 502
761 description = (
762 "The proxy server received an invalid response from an upstream server."
763 )
764
765
766class ServiceUnavailable(_RetryAfter):
767 """*503* `Service Unavailable`
768
769 Status code you should return if a service is temporarily
770 unavailable.
771
772 :param retry_after: If given, set the ``Retry-After`` header to this
773 value. May be an :class:`int` number of seconds or a
774 :class:`~datetime.datetime`.
775
776 .. versionchanged:: 1.0
777 Added ``retry_after`` parameter.
778 """
779
780 code = 503
781 description = (
782 "The server is temporarily unable to service your request due"
783 " to maintenance downtime or capacity problems. Please try"
784 " again later."
785 )
786
787
788class GatewayTimeout(HTTPException):
789 """*504* `Gateway Timeout`
790
791 Status code you should return if a connection to an upstream server
792 times out.
793 """
794
795 code = 504
796 description = "The connection to an upstream server timed out."
797
798
799class HTTPVersionNotSupported(HTTPException):
800 """*505* `HTTP Version Not Supported`
801
802 The server does not support the HTTP protocol version used in the request.
803 """
804
805 code = 505
806 description = (
807 "The server does not support the HTTP protocol version used in the request."
808 )
809
810
811default_exceptions: dict[int, type[HTTPException]] = {}
812
813
814def _find_exceptions() -> None:
815 for obj in globals().values():
816 try:
817 is_http_exception = issubclass(obj, HTTPException)
818 except TypeError:
819 is_http_exception = False
820 if not is_http_exception or obj.code is None:
821 continue
822 old_obj = default_exceptions.get(obj.code, None)
823 if old_obj is not None and issubclass(obj, old_obj):
824 continue
825 default_exceptions[obj.code] = obj
826
827
828_find_exceptions()
829del _find_exceptions
830
831
832class Aborter:
833 """When passed a dict of code -> exception items it can be used as
834 callable that raises exceptions. If the first argument to the
835 callable is an integer it will be looked up in the mapping, if it's
836 a WSGI application it will be raised in a proxy exception.
837
838 The rest of the arguments are forwarded to the exception constructor.
839 """
840
841 def __init__(
842 self,
843 mapping: dict[int, type[HTTPException]] | None = None,
844 extra: dict[int, type[HTTPException]] | None = None,
845 ) -> None:
846 if mapping is None:
847 mapping = default_exceptions
848 self.mapping = dict(mapping)
849 if extra is not None:
850 self.mapping.update(extra)
851
852 def __call__(
853 self, code: int | Response, *args: t.Any, **kwargs: t.Any
854 ) -> t.NoReturn:
855 from .sansio.response import Response
856
857 if isinstance(code, Response):
858 raise HTTPException(response=code)
859
860 if code not in self.mapping:
861 raise LookupError(f"no exception for {code!r}")
862
863 raise self.mapping[code](*args, **kwargs)
864
865
866def abort(status: int | Response, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
867 """Raises an :py:exc:`HTTPException` for the given status code or WSGI
868 application.
869
870 If a status code is given, it will be looked up in the list of
871 exceptions and will raise that exception. If passed a WSGI application,
872 it will wrap it in a proxy WSGI exception and raise that::
873
874 abort(404) # 404 Not Found
875 abort(Response('Hello World'))
876
877 """
878 _aborter(status, *args, **kwargs)
879
880
881_aborter: Aborter = Aborter()