1"""
2This module defines Exception classes used by Connexion to generate a proper response.
3"""
4
5import typing as t
6
7from jsonschema.exceptions import ValidationError
8from starlette.exceptions import HTTPException
9
10from .problem import problem
11
12
13class ConnexionException(Exception):
14 """Base class for any exception thrown by the Connexion framework."""
15
16
17class ResolverError(LookupError, ConnexionException):
18 """Error raised at startup when the resolver cannot find a view function for an endpoint in
19 your specification, and no ``resolver_error`` is configured."""
20
21
22class InvalidSpecification(ValidationError, ConnexionException):
23 """Error raised at startup when the provided specification cannot be validated."""
24
25
26class MissingMiddleware(ConnexionException):
27 """Error raised when you're leveraging behavior that depends on a specific middleware,
28 and that middleware is not part of your middleware stack."""
29
30
31# HTTP ERRORS
32
33
34class ProblemException(HTTPException, ConnexionException):
35 """
36 This exception holds arguments that are going to be passed to the
37 `connexion.problem` function to generate a proper response.
38 """
39
40 def __init__(
41 self,
42 *,
43 status=500,
44 title=None,
45 detail=None,
46 type=None,
47 instance=None,
48 headers=None,
49 ext=None,
50 ):
51 self.status = self.status_code = status
52 self.title = title
53 self.detail = detail
54 self.type = type
55 self.instance = instance
56 self.headers = headers
57 self.ext = ext
58
59 def to_problem(self):
60 return problem(
61 status=self.status,
62 title=self.title,
63 detail=self.detail,
64 type=self.type,
65 instance=self.instance,
66 headers=self.headers,
67 ext=self.ext,
68 )
69
70
71# CLIENT ERRORS (4XX)
72
73
74class ClientProblem(ProblemException):
75 """Base exception for any 4XX error. Returns 400 by default, however
76 :class:`BadRequestProblem` should be preferred for 400 errors."""
77
78 def __init__(
79 self,
80 status: int = 400,
81 title: t.Optional[str] = None,
82 *,
83 detail: t.Optional[str] = None,
84 ):
85 super().__init__(status=status, title=title, detail=detail)
86
87
88class BadRequestProblem(ClientProblem):
89 """Problem class for 400 Bad Request errors."""
90
91 def __init__(self, detail=None):
92 super().__init__(status=400, title="Bad Request", detail=detail)
93
94
95class ExtraParameterProblem(BadRequestProblem):
96 """Problem class for 400 Bad Request errors raised when extra query or form parameters are
97 detected and ``strict_validation`` is enabled."""
98
99 def __init__(self, *, param_type: str, extra_params: t.Iterable[str]):
100 detail = f"Extra {param_type} parameter(s) {','.join(extra_params)} not in spec"
101 super().__init__(detail=detail)
102
103
104class TypeValidationError(BadRequestProblem):
105 """Problem class for 400 Bad Request errors raised when path, query or form parameters with
106 an incorrect type are detected."""
107
108 def __init__(self, schema_type: str, parameter_type: str, parameter_name: str):
109 detail = f"Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
110 super().__init__(detail=detail)
111
112
113class Unauthorized(ClientProblem):
114 """Problem class for 401 Unauthorized errors."""
115
116 description = (
117 "The server could not verify that you are authorized to access"
118 " the URL requested. You either supplied the wrong credentials"
119 " (e.g. a bad password), or your browser doesn't understand"
120 " how to supply the credentials required."
121 )
122
123 def __init__(self, detail: str = description):
124 super().__init__(401, title="Unauthorized", detail=detail)
125
126
127class OAuthProblem(Unauthorized):
128 """Problem class for 401 Unauthorized errors raised when there is an issue with the received
129 OAuth headers."""
130
131 pass
132
133
134class OAuthResponseProblem(OAuthProblem):
135 """Problem class for 401 Unauthorized errors raised when improper OAuth credentials are
136 retrieved from your OAuth server."""
137
138 pass
139
140
141class Forbidden(HTTPException):
142 """Problem class for 403 Unauthorized errors."""
143
144 def __init__(self, detail: t.Optional[str] = None):
145 if detail is None:
146 detail = (
147 "You don't have the permission to access the requested"
148 " resource. It is either read-protected or not readable by the"
149 " server."
150 )
151 super().__init__(403, detail=detail)
152
153
154class OAuthScopeProblem(Forbidden):
155 """Problem class for 403 Unauthorized errors raised because of OAuth scope validation errors."""
156
157 def __init__(self, token_scopes: list, required_scopes: list) -> None:
158 self.required_scopes = required_scopes
159 self.token_scopes = token_scopes
160 detail = (
161 f"Provided token does not have the required scopes. "
162 f"Provided: {token_scopes}; Required: {required_scopes}"
163 )
164 super().__init__(detail=detail)
165
166
167class UnsupportedMediaTypeProblem(ClientProblem):
168 """Problem class for 415 Unsupported Media Type errors which are raised when Connexion
169 receives a request with an unsupported media type header."""
170
171 def __init__(self, detail: t.Optional[str] = None):
172 super().__init__(status=415, title="Unsupported Media Type", detail=detail)
173
174
175# SERVER ERRORS (5XX)
176
177
178class ServerError(ProblemException):
179 """Base exception for any 5XX error. Returns 500 by default, however
180 :class:`InternalServerError` should be preferred for 500 errors."""
181
182 def __init__(
183 self,
184 status: int = 500,
185 title: t.Optional[str] = None,
186 *,
187 detail: t.Optional[str] = None,
188 ):
189 if title is None:
190 title = "Internal Server Error"
191
192 super().__init__(status=status, title=title, detail=detail)
193
194
195class InternalServerError(ServerError):
196 """Problem class for 500 Internal Server errors."""
197
198 def __init__(self, detail: t.Optional[str] = None):
199 if detail is None:
200 detail = (
201 "The server encountered an internal error and was unable to complete your "
202 "request. Either the server is overloaded or there is an error in the application."
203 )
204 super().__init__(status=500, title="Internal Server Error", detail=detail)
205
206
207class NonConformingResponse(InternalServerError):
208 """Problem class for 500 Internal Server errors raised because of a returned response not
209 matching the specification if response validation is enabled."""
210
211 def __init__(self, detail: t.Optional[str] = None):
212 super().__init__(detail=detail)
213
214
215class NonConformingResponseBody(NonConformingResponse):
216 """Problem class for 500 Internal Server errors raised because of a returned response body not
217 matching the specification if response validation is enabled."""
218
219 def __init__(self, detail: t.Optional[str] = None):
220 if detail is None:
221 detail = "Response body does not conform to specification"
222
223 super().__init__(detail=detail)
224
225
226class NonConformingResponseHeaders(NonConformingResponse):
227 """Problem class for 500 Internal Server errors raised because of a returned response headers
228 not matching the specification if response validation is enabled."""
229
230 def __init__(self, detail: t.Optional[str] = None):
231 if detail is None:
232 detail = "Response headers do not conform to specification"
233
234 super().__init__(detail=detail)
235
236
237class ResolverProblem(ServerError):
238 """Problem class for 501 Not Implemented errors raised when the resolver cannot find a view
239 function to handle the incoming request."""
240
241 def __init__(self, status: int = 501, *, detail: t.Optional[str] = None):
242 super().__init__(status=status, title="Not Implemented", detail=detail)