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__(self, status: int = 400, title: str = None, *, detail: str = None):
79 super().__init__(status=status, title=title, detail=detail)
80
81
82class BadRequestProblem(ClientProblem):
83 """Problem class for 400 Bad Request errors."""
84
85 def __init__(self, detail=None):
86 super().__init__(status=400, title="Bad Request", detail=detail)
87
88
89class ExtraParameterProblem(BadRequestProblem):
90 """Problem class for 400 Bad Request errors raised when extra query or form parameters are
91 detected and ``strict_validation`` is enabled."""
92
93 def __init__(self, *, param_type: str, extra_params: t.Iterable[str]):
94 detail = f"Extra {param_type} parameter(s) {','.join(extra_params)} not in spec"
95 super().__init__(detail=detail)
96
97
98class TypeValidationError(BadRequestProblem):
99 """Problem class for 400 Bad Request errors raised when path, query or form parameters with
100 an incorrect type are detected."""
101
102 def __init__(self, schema_type: str, parameter_type: str, parameter_name: str):
103 detail = f"Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
104 super().__init__(detail=detail)
105
106
107class Unauthorized(ClientProblem):
108 """Problem class for 401 Unauthorized errors."""
109
110 description = (
111 "The server could not verify that you are authorized to access"
112 " the URL requested. You either supplied the wrong credentials"
113 " (e.g. a bad password), or your browser doesn't understand"
114 " how to supply the credentials required."
115 )
116
117 def __init__(self, detail: str = description):
118 super().__init__(401, title="Unauthorized", detail=detail)
119
120
121class OAuthProblem(Unauthorized):
122 """Problem class for 401 Unauthorized errors raised when there is an issue with the received
123 OAuth headers."""
124
125 pass
126
127
128class OAuthResponseProblem(OAuthProblem):
129 """Problem class for 401 Unauthorized errors raised when improper OAuth credentials are
130 retrieved from your OAuth server."""
131
132 pass
133
134
135class Forbidden(HTTPException):
136 """Problem class for 403 Unauthorized errors."""
137
138 def __init__(self, detail: t.Optional[str] = None):
139 if detail is None:
140 detail = (
141 "You don't have the permission to access the requested"
142 " resource. It is either read-protected or not readable by the"
143 " server."
144 )
145 super().__init__(403, detail=detail)
146
147
148class OAuthScopeProblem(Forbidden):
149 """Problem class for 403 Unauthorized errors raised because of OAuth scope validation errors."""
150
151 def __init__(self, token_scopes: list, required_scopes: list) -> None:
152 self.required_scopes = required_scopes
153 self.token_scopes = token_scopes
154 detail = (
155 f"Provided token does not have the required scopes. "
156 f"Provided: {token_scopes}; Required: {required_scopes}"
157 )
158 super().__init__(detail=detail)
159
160
161class UnsupportedMediaTypeProblem(ClientProblem):
162 """Problem class for 415 Unsupported Media Type errors which are raised when Connexion
163 receives a request with an unsupported media type header."""
164
165 def __init__(self, detail: t.Optional[str] = None):
166 super().__init__(status=415, title="Unsupported Media Type", detail=detail)
167
168
169# SERVER ERRORS (5XX)
170
171
172class ServerError(ProblemException):
173 """Base exception for any 5XX error. Returns 500 by default, however
174 :class:`InternalServerError` should be preferred for 500 errors."""
175
176 def __init__(
177 self,
178 status: int = 500,
179 title: t.Optional[str] = None,
180 *,
181 detail: t.Optional[str] = None,
182 ):
183 if title is None:
184 title = "Internal Server Error"
185
186 super().__init__(status=status, title=title, detail=detail)
187
188
189class InternalServerError(ServerError):
190 """Problem class for 500 Internal Server errors."""
191
192 def __init__(self, detail: t.Optional[str] = None):
193 if detail is None:
194 detail = (
195 "The server encountered an internal error and was unable to complete your "
196 "request. Either the server is overloaded or there is an error in the application."
197 )
198 super().__init__(status=500, title="Internal Server Error", detail=detail)
199
200
201class NonConformingResponse(InternalServerError):
202 """Problem class for 500 Internal Server errors raised because of a returned response not
203 matching the specification if response validation is enabled."""
204
205 def __init__(self, detail: t.Optional[str] = None):
206 super().__init__(detail=detail)
207
208
209class NonConformingResponseBody(NonConformingResponse):
210 """Problem class for 500 Internal Server errors raised because of a returned response body not
211 matching the specification if response validation is enabled."""
212
213 def __init__(self, detail: t.Optional[str] = None):
214 if detail is None:
215 detail = "Response body does not conform to specification"
216
217 super().__init__(detail=detail)
218
219
220class NonConformingResponseHeaders(NonConformingResponse):
221 """Problem class for 500 Internal Server errors raised because of a returned response headers
222 not matching the specification if response validation is enabled."""
223
224 def __init__(self, detail: t.Optional[str] = None):
225 if detail is None:
226 detail = "Response headers do not conform to specification"
227
228 super().__init__(detail=detail)
229
230
231class ResolverProblem(ServerError):
232 """Problem class for 501 Not Implemented errors raised when the resolver cannot find a view
233 function to handle the incoming request."""
234
235 def __init__(self, status: int = 501, *, detail: t.Optional[str] = None):
236 super().__init__(status=status, title="Not Implemented", detail=detail)