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: object | 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 MisdirectedRequest(HTTPException):
575 """421 Misdirected Request
576
577 Indicates that the request was directed to a server that is not able to
578 produce a response.
579
580 .. versionadded:: 3.1
581 """
582
583 code = 421
584 description = "The server is not able to produce a response."
585
586
587class UnprocessableEntity(HTTPException):
588 """*422* `Unprocessable Entity`
589
590 Used if the request is well formed, but the instructions are otherwise
591 incorrect.
592 """
593
594 code = 422
595 description = (
596 "The request was well-formed but was unable to be followed due"
597 " to semantic errors."
598 )
599
600
601class Locked(HTTPException):
602 """*423* `Locked`
603
604 Used if the resource that is being accessed is locked.
605 """
606
607 code = 423
608 description = "The resource that is being accessed is locked."
609
610
611class FailedDependency(HTTPException):
612 """*424* `Failed Dependency`
613
614 Used if the method could not be performed on the resource
615 because the requested action depended on another action and that action failed.
616 """
617
618 code = 424
619 description = (
620 "The method could not be performed on the resource because the"
621 " requested action depended on another action and that action"
622 " failed."
623 )
624
625
626class PreconditionRequired(HTTPException):
627 """*428* `Precondition Required`
628
629 The server requires this request to be conditional, typically to prevent
630 the lost update problem, which is a race condition between two or more
631 clients attempting to update a resource through PUT or DELETE. By requiring
632 each client to include a conditional header ("If-Match" or "If-Unmodified-
633 Since") with the proper value retained from a recent GET request, the
634 server ensures that each client has at least seen the previous revision of
635 the resource.
636 """
637
638 code = 428
639 description = (
640 "This request is required to be conditional; try using"
641 ' "If-Match" or "If-Unmodified-Since".'
642 )
643
644
645class _RetryAfter(HTTPException):
646 """Adds an optional ``retry_after`` parameter which will set the
647 ``Retry-After`` header. May be an :class:`int` number of seconds or
648 a :class:`~datetime.datetime`.
649 """
650
651 def __init__(
652 self,
653 description: str | None = None,
654 response: Response | None = None,
655 retry_after: datetime | int | None = None,
656 ) -> None:
657 super().__init__(description, response)
658 self.retry_after = retry_after
659
660 def get_headers(
661 self,
662 environ: WSGIEnvironment | None = None,
663 scope: dict[str, t.Any] | None = None,
664 ) -> list[tuple[str, str]]:
665 headers = super().get_headers(environ, scope)
666
667 if self.retry_after:
668 if isinstance(self.retry_after, datetime):
669 from .http import http_date
670
671 value = http_date(self.retry_after)
672 else:
673 value = str(self.retry_after)
674
675 headers.append(("Retry-After", value))
676
677 return headers
678
679
680class TooManyRequests(_RetryAfter):
681 """*429* `Too Many Requests`
682
683 The server is limiting the rate at which this user receives
684 responses, and this request exceeds that rate. (The server may use
685 any convenient method to identify users and their request rates).
686 The server may include a "Retry-After" header to indicate how long
687 the user should wait before retrying.
688
689 :param retry_after: If given, set the ``Retry-After`` header to this
690 value. May be an :class:`int` number of seconds or a
691 :class:`~datetime.datetime`.
692
693 .. versionchanged:: 1.0
694 Added ``retry_after`` parameter.
695 """
696
697 code = 429
698 description = "This user has exceeded an allotted request count. Try again later."
699
700
701class RequestHeaderFieldsTooLarge(HTTPException):
702 """*431* `Request Header Fields Too Large`
703
704 The server refuses to process the request because the header fields are too
705 large. One or more individual fields may be too large, or the set of all
706 headers is too large.
707 """
708
709 code = 431
710 description = "One or more header fields exceeds the maximum size."
711
712
713class UnavailableForLegalReasons(HTTPException):
714 """*451* `Unavailable For Legal Reasons`
715
716 This status code indicates that the server is denying access to the
717 resource as a consequence of a legal demand.
718 """
719
720 code = 451
721 description = "Unavailable for legal reasons."
722
723
724class InternalServerError(HTTPException):
725 """*500* `Internal Server Error`
726
727 Raise if an internal server error occurred. This is a good fallback if an
728 unknown error occurred in the dispatcher.
729
730 .. versionchanged:: 1.0.0
731 Added the :attr:`original_exception` attribute.
732 """
733
734 code = 500
735 description = (
736 "The server encountered an internal error and was unable to"
737 " complete your request. Either the server is overloaded or"
738 " there is an error in the application."
739 )
740
741 def __init__(
742 self,
743 description: str | None = None,
744 response: Response | None = None,
745 original_exception: BaseException | None = None,
746 ) -> None:
747 #: The original exception that caused this 500 error. Can be
748 #: used by frameworks to provide context when handling
749 #: unexpected errors.
750 self.original_exception = original_exception
751 super().__init__(description=description, response=response)
752
753
754class NotImplemented(HTTPException):
755 """*501* `Not Implemented`
756
757 Raise if the application does not support the action requested by the
758 browser.
759 """
760
761 code = 501
762 description = "The server does not support the action requested by the browser."
763
764
765class BadGateway(HTTPException):
766 """*502* `Bad Gateway`
767
768 If you do proxying in your application you should return this status code
769 if you received an invalid response from the upstream server it accessed
770 in attempting to fulfill the request.
771 """
772
773 code = 502
774 description = (
775 "The proxy server received an invalid response from an upstream server."
776 )
777
778
779class ServiceUnavailable(_RetryAfter):
780 """*503* `Service Unavailable`
781
782 Status code you should return if a service is temporarily
783 unavailable.
784
785 :param retry_after: If given, set the ``Retry-After`` header to this
786 value. May be an :class:`int` number of seconds or a
787 :class:`~datetime.datetime`.
788
789 .. versionchanged:: 1.0
790 Added ``retry_after`` parameter.
791 """
792
793 code = 503
794 description = (
795 "The server is temporarily unable to service your request due"
796 " to maintenance downtime or capacity problems. Please try"
797 " again later."
798 )
799
800
801class GatewayTimeout(HTTPException):
802 """*504* `Gateway Timeout`
803
804 Status code you should return if a connection to an upstream server
805 times out.
806 """
807
808 code = 504
809 description = "The connection to an upstream server timed out."
810
811
812class HTTPVersionNotSupported(HTTPException):
813 """*505* `HTTP Version Not Supported`
814
815 The server does not support the HTTP protocol version used in the request.
816 """
817
818 code = 505
819 description = (
820 "The server does not support the HTTP protocol version used in the request."
821 )
822
823
824default_exceptions: dict[int, type[HTTPException]] = {}
825
826
827def _find_exceptions() -> None:
828 for obj in globals().values():
829 try:
830 is_http_exception = issubclass(obj, HTTPException)
831 except TypeError:
832 is_http_exception = False
833 if not is_http_exception or obj.code is None:
834 continue
835 old_obj = default_exceptions.get(obj.code, None)
836 if old_obj is not None and issubclass(obj, old_obj):
837 continue
838 default_exceptions[obj.code] = obj
839
840
841_find_exceptions()
842del _find_exceptions
843
844
845class Aborter:
846 """When passed a dict of code -> exception items it can be used as
847 callable that raises exceptions. If the first argument to the
848 callable is an integer it will be looked up in the mapping, if it's
849 a WSGI application it will be raised in a proxy exception.
850
851 The rest of the arguments are forwarded to the exception constructor.
852 """
853
854 def __init__(
855 self,
856 mapping: dict[int, type[HTTPException]] | None = None,
857 extra: dict[int, type[HTTPException]] | None = None,
858 ) -> None:
859 if mapping is None:
860 mapping = default_exceptions
861 self.mapping = dict(mapping)
862 if extra is not None:
863 self.mapping.update(extra)
864
865 def __call__(
866 self, code: int | Response, *args: t.Any, **kwargs: t.Any
867 ) -> t.NoReturn:
868 from .sansio.response import Response
869
870 if isinstance(code, Response):
871 raise HTTPException(response=code)
872
873 if code not in self.mapping:
874 raise LookupError(f"no exception for {code!r}")
875
876 raise self.mapping[code](*args, **kwargs)
877
878
879def abort(status: int | Response, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
880 """Raises an :py:exc:`HTTPException` for the given status code or WSGI
881 application.
882
883 If a status code is given, it will be looked up in the list of
884 exceptions and will raise that exception. If passed a WSGI application,
885 it will wrap it in a proxy WSGI exception and raise that::
886
887 abort(404) # 404 Not Found
888 abort(Response('Hello World'))
889
890 """
891 _aborter(status, *args, **kwargs)
892
893
894_aborter: Aborter = Aborter()