36
36
OpenAPIResponse ,
37
37
OpenAPIResponseContentModel ,
38
38
OpenAPIResponseContentSchema ,
39
+ response_validation_error_response_definition ,
39
40
validation_error_definition ,
40
41
validation_error_response_definition ,
41
42
)
@@ -320,6 +321,7 @@ def __init__(
320
321
security : list [dict [str , list [str ]]] | None = None ,
321
322
openapi_extensions : dict [str , Any ] | None = None ,
322
323
deprecated : bool = False ,
324
+ custom_response_validation_http_code : HTTPStatus | None = None ,
323
325
middlewares : list [Callable [..., Response ]] | None = None ,
324
326
):
325
327
"""
@@ -361,11 +363,13 @@ def __init__(
361
363
Additional OpenAPI extensions as a dictionary.
362
364
deprecated: bool
363
365
Whether or not to mark this route as deprecated in the OpenAPI schema
366
+ custom_response_validation_http_code: int | HTTPStatus | None, optional
367
+ Whether to have custom http status code for this route if response validation fails
364
368
middlewares: list[Callable[..., Response]] | None
365
369
The list of route middlewares to be called in order.
366
370
"""
367
371
self .method = method .upper ()
368
- self .path = "/" if path .strip () == "" else path
372
+ self .path = path if path .strip () else "/"
369
373
370
374
# OpenAPI spec only understands paths with { }. So we'll have to convert Powertools' < >.
371
375
# https://swagger.io/specification/#path-templating
@@ -398,6 +402,8 @@ def __init__(
398
402
# _body_field is used to cache the dependant model for the body field
399
403
self ._body_field : ModelField | None = None
400
404
405
+ self .custom_response_validation_http_code = custom_response_validation_http_code
406
+
401
407
def __call__ (
402
408
self ,
403
409
router_middlewares : list [Callable ],
@@ -566,6 +572,16 @@ def _get_openapi_path(
566
572
},
567
573
}
568
574
575
+ # Add custom response validation response, if exists
576
+ if self .custom_response_validation_http_code :
577
+ http_code = self .custom_response_validation_http_code .value
578
+ operation_responses [http_code ] = {
579
+ "description" : "Response Validation Error" ,
580
+ "content" : {"application/json" : {"schema" : {"$ref" : f"{ COMPONENT_REF_PREFIX } ResponseValidationError" }}},
581
+ }
582
+ # Add model definition
583
+ definitions ["ResponseValidationError" ] = response_validation_error_response_definition
584
+
569
585
# Add the response to the OpenAPI operation
570
586
if self .responses :
571
587
for status_code in list (self .responses ):
@@ -943,6 +959,7 @@ def route(
943
959
security : list [dict [str , list [str ]]] | None = None ,
944
960
openapi_extensions : dict [str , Any ] | None = None ,
945
961
deprecated : bool = False ,
962
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
946
963
middlewares : list [Callable [..., Any ]] | None = None ,
947
964
) -> Callable [[AnyCallableT ], AnyCallableT ]:
948
965
raise NotImplementedError ()
@@ -1004,6 +1021,7 @@ def get(
1004
1021
security : list [dict [str , list [str ]]] | None = None ,
1005
1022
openapi_extensions : dict [str , Any ] | None = None ,
1006
1023
deprecated : bool = False ,
1024
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1007
1025
middlewares : list [Callable [..., Any ]] | None = None ,
1008
1026
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1009
1027
"""Get route decorator with GET `method`
@@ -1044,6 +1062,7 @@ def lambda_handler(event, context):
1044
1062
security ,
1045
1063
openapi_extensions ,
1046
1064
deprecated ,
1065
+ custom_response_validation_http_code ,
1047
1066
middlewares ,
1048
1067
)
1049
1068
@@ -1063,6 +1082,7 @@ def post(
1063
1082
security : list [dict [str , list [str ]]] | None = None ,
1064
1083
openapi_extensions : dict [str , Any ] | None = None ,
1065
1084
deprecated : bool = False ,
1085
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1066
1086
middlewares : list [Callable [..., Any ]] | None = None ,
1067
1087
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1068
1088
"""Post route decorator with POST `method`
@@ -1104,6 +1124,7 @@ def lambda_handler(event, context):
1104
1124
security ,
1105
1125
openapi_extensions ,
1106
1126
deprecated ,
1127
+ custom_response_validation_http_code ,
1107
1128
middlewares ,
1108
1129
)
1109
1130
@@ -1123,6 +1144,7 @@ def put(
1123
1144
security : list [dict [str , list [str ]]] | None = None ,
1124
1145
openapi_extensions : dict [str , Any ] | None = None ,
1125
1146
deprecated : bool = False ,
1147
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1126
1148
middlewares : list [Callable [..., Any ]] | None = None ,
1127
1149
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1128
1150
"""Put route decorator with PUT `method`
@@ -1164,6 +1186,7 @@ def lambda_handler(event, context):
1164
1186
security ,
1165
1187
openapi_extensions ,
1166
1188
deprecated ,
1189
+ custom_response_validation_http_code ,
1167
1190
middlewares ,
1168
1191
)
1169
1192
@@ -1183,6 +1206,7 @@ def delete(
1183
1206
security : list [dict [str , list [str ]]] | None = None ,
1184
1207
openapi_extensions : dict [str , Any ] | None = None ,
1185
1208
deprecated : bool = False ,
1209
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1186
1210
middlewares : list [Callable [..., Any ]] | None = None ,
1187
1211
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1188
1212
"""Delete route decorator with DELETE `method`
@@ -1223,6 +1247,7 @@ def lambda_handler(event, context):
1223
1247
security ,
1224
1248
openapi_extensions ,
1225
1249
deprecated ,
1250
+ custom_response_validation_http_code ,
1226
1251
middlewares ,
1227
1252
)
1228
1253
@@ -1242,6 +1267,7 @@ def patch(
1242
1267
security : list [dict [str , list [str ]]] | None = None ,
1243
1268
openapi_extensions : dict [str , Any ] | None = None ,
1244
1269
deprecated : bool = False ,
1270
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1245
1271
middlewares : list [Callable ] | None = None ,
1246
1272
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1247
1273
"""Patch route decorator with PATCH `method`
@@ -1285,6 +1311,7 @@ def lambda_handler(event, context):
1285
1311
security ,
1286
1312
openapi_extensions ,
1287
1313
deprecated ,
1314
+ custom_response_validation_http_code ,
1288
1315
middlewares ,
1289
1316
)
1290
1317
@@ -1304,6 +1331,7 @@ def head(
1304
1331
security : list [dict [str , list [str ]]] | None = None ,
1305
1332
openapi_extensions : dict [str , Any ] | None = None ,
1306
1333
deprecated : bool = False ,
1334
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1307
1335
middlewares : list [Callable ] | None = None ,
1308
1336
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1309
1337
"""Head route decorator with HEAD `method`
@@ -1346,6 +1374,7 @@ def lambda_handler(event, context):
1346
1374
security ,
1347
1375
openapi_extensions ,
1348
1376
deprecated ,
1377
+ custom_response_validation_http_code ,
1349
1378
middlewares ,
1350
1379
)
1351
1380
@@ -1573,6 +1602,7 @@ def _validate_response_validation_error_http_code(
1573
1602
response_validation_error_http_code : HTTPStatus | int | None ,
1574
1603
enable_validation : bool ,
1575
1604
) -> HTTPStatus :
1605
+
1576
1606
if response_validation_error_http_code and not enable_validation :
1577
1607
msg = "'response_validation_error_http_code' cannot be set when enable_validation is False."
1578
1608
raise ValueError (msg )
@@ -1590,6 +1620,33 @@ def _validate_response_validation_error_http_code(
1590
1620
1591
1621
return response_validation_error_http_code or HTTPStatus .UNPROCESSABLE_ENTITY
1592
1622
1623
+ def _add_resolver_response_validation_error_response_to_route (
1624
+ self ,
1625
+ route_openapi_path : tuple [dict [str , Any ], dict [str , Any ]],
1626
+ ) -> tuple [dict [str , Any ], dict [str , Any ]]:
1627
+ """Adds resolver response validation error response to route's operations."""
1628
+ path , path_definitions = route_openapi_path
1629
+ if self ._has_response_validation_error and "ResponseValidationError" not in path_definitions :
1630
+ response_validation_error_response = {
1631
+ "description" : "Response Validation Error" ,
1632
+ "content" : {
1633
+ "application/json" : {
1634
+ "schema" : {"$ref" : f"{ COMPONENT_REF_PREFIX } ResponseValidationError" },
1635
+ },
1636
+ },
1637
+ }
1638
+ http_code = self ._response_validation_error_http_code .value
1639
+ for operation in path .values ():
1640
+ operation ["responses" ][http_code ] = response_validation_error_response
1641
+ return path , path_definitions
1642
+
1643
+ def _generate_schemas (self , definitions : dict [str , dict [str , Any ]]) -> dict [str , dict [str , Any ]]:
1644
+ schemas = {k : definitions [k ] for k in sorted (definitions )}
1645
+ # add response validation error definition
1646
+ if self ._response_validation_error_http_code :
1647
+ schemas .setdefault ("ResponseValidationError" , response_validation_error_response_definition )
1648
+ return schemas
1649
+
1593
1650
def get_openapi_schema (
1594
1651
self ,
1595
1652
* ,
@@ -1741,14 +1798,14 @@ def get_openapi_schema(
1741
1798
field_mapping = field_mapping ,
1742
1799
)
1743
1800
if result :
1744
- path , path_definitions = result
1801
+ path , path_definitions = self . _add_resolver_response_validation_error_response_to_route ( result )
1745
1802
if path :
1746
1803
paths .setdefault (route .openapi_path , {}).update (path )
1747
1804
if path_definitions :
1748
1805
definitions .update (path_definitions )
1749
1806
1750
1807
if definitions :
1751
- components ["schemas" ] = { k : definitions [ k ] for k in sorted (definitions )}
1808
+ components ["schemas" ] = self . _generate_schemas (definitions )
1752
1809
if security_schemes :
1753
1810
components ["securitySchemes" ] = security_schemes
1754
1811
if components :
@@ -2110,6 +2167,29 @@ def swagger_handler():
2110
2167
body = body ,
2111
2168
)
2112
2169
2170
+ def _validate_route_response_validation_error_http_code (
2171
+ self ,
2172
+ custom_response_validation_http_code : int | HTTPStatus | None ,
2173
+ ) -> HTTPStatus | None :
2174
+ if custom_response_validation_http_code and not self ._enable_validation :
2175
+ msg = (
2176
+ "'custom_response_validation_http_code' cannot be set for route when enable_validation is False "
2177
+ "on resolver."
2178
+ )
2179
+ raise ValueError (msg )
2180
+
2181
+ if (
2182
+ not isinstance (custom_response_validation_http_code , HTTPStatus )
2183
+ and custom_response_validation_http_code is not None
2184
+ ):
2185
+ try :
2186
+ custom_response_validation_http_code = HTTPStatus (custom_response_validation_http_code )
2187
+ except ValueError :
2188
+ msg = f"'{ custom_response_validation_http_code } ' must be an integer representing an HTTP status code or an enum of type HTTPStatus." # noqa: E501
2189
+ raise ValueError (msg ) from None
2190
+
2191
+ return custom_response_validation_http_code
2192
+
2113
2193
def route (
2114
2194
self ,
2115
2195
rule : str ,
@@ -2127,10 +2207,15 @@ def route(
2127
2207
security : list [dict [str , list [str ]]] | None = None ,
2128
2208
openapi_extensions : dict [str , Any ] | None = None ,
2129
2209
deprecated : bool = False ,
2210
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2130
2211
middlewares : list [Callable [..., Any ]] | None = None ,
2131
2212
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2132
2213
"""Route decorator includes parameter `method`"""
2133
2214
2215
+ custom_response_validation_http_code = self ._validate_route_response_validation_error_http_code (
2216
+ custom_response_validation_http_code ,
2217
+ )
2218
+
2134
2219
def register_resolver (func : AnyCallableT ) -> AnyCallableT :
2135
2220
methods = (method ,) if isinstance (method , str ) else method
2136
2221
logger .debug (f"Adding route using rule { rule } and methods: { ',' .join (m .upper () for m in methods )} " )
@@ -2156,6 +2241,7 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT:
2156
2241
security ,
2157
2242
openapi_extensions ,
2158
2243
deprecated ,
2244
+ custom_response_validation_http_code ,
2159
2245
middlewares ,
2160
2246
)
2161
2247
@@ -2509,15 +2595,17 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuild
2509
2595
)
2510
2596
2511
2597
# OpenAPIValidationMiddleware will only raise ResponseValidationError when
2512
- # 'self._response_validation_error_http_code' is not None
2598
+ # 'self._response_validation_error_http_code' is not None or
2599
+ # when route has custom_response_validation_http_code
2513
2600
if isinstance (exp , ResponseValidationError ):
2514
- http_code = self ._response_validation_error_http_code
2601
+ # route validation must take precedence over app validation
2602
+ http_code = route .custom_response_validation_http_code or self ._response_validation_error_http_code
2515
2603
errors = [{"loc" : e ["loc" ], "type" : e ["type" ]} for e in exp .errors ()]
2516
2604
return self ._response_builder_class (
2517
2605
response = Response (
2518
2606
status_code = http_code .value ,
2519
2607
content_type = content_types .APPLICATION_JSON ,
2520
- body = {"statusCode" : self . _response_validation_error_http_code , "detail" : errors },
2608
+ body = {"statusCode" : http_code , "detail" : errors },
2521
2609
),
2522
2610
serializer = self ._serializer ,
2523
2611
route = route ,
@@ -2668,6 +2756,7 @@ def route(
2668
2756
security : list [dict [str , list [str ]]] | None = None ,
2669
2757
openapi_extensions : dict [str , Any ] | None = None ,
2670
2758
deprecated : bool = False ,
2759
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2671
2760
middlewares : list [Callable [..., Any ]] | None = None ,
2672
2761
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2673
2762
def register_route (func : AnyCallableT ) -> AnyCallableT :
@@ -2694,6 +2783,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT:
2694
2783
frozen_security ,
2695
2784
frozen_openapi_extensions ,
2696
2785
deprecated ,
2786
+ custom_response_validation_http_code ,
2697
2787
)
2698
2788
2699
2789
# Collate Middleware for routes
@@ -2780,6 +2870,7 @@ def route(
2780
2870
security : list [dict [str , list [str ]]] | None = None ,
2781
2871
openapi_extensions : dict [str , Any ] | None = None ,
2782
2872
deprecated : bool = False ,
2873
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2783
2874
middlewares : list [Callable [..., Any ]] | None = None ,
2784
2875
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2785
2876
# NOTE: see #1552 for more context.
@@ -2799,6 +2890,7 @@ def route(
2799
2890
security ,
2800
2891
openapi_extensions ,
2801
2892
deprecated ,
2893
+ custom_response_validation_http_code ,
2802
2894
middlewares ,
2803
2895
)
2804
2896
0 commit comments