35
35
OpenAPIResponse ,
36
36
OpenAPIResponseContentModel ,
37
37
OpenAPIResponseContentSchema ,
38
+ response_validation_error_response_definition ,
38
39
validation_error_definition ,
39
40
validation_error_response_definition ,
40
41
)
@@ -319,6 +320,7 @@ def __init__(
319
320
security : list [dict [str , list [str ]]] | None = None ,
320
321
openapi_extensions : dict [str , Any ] | None = None ,
321
322
deprecated : bool = False ,
323
+ custom_response_validation_http_code : HTTPStatus | None = None ,
322
324
middlewares : list [Callable [..., Response ]] | None = None ,
323
325
):
324
326
"""
@@ -360,11 +362,13 @@ def __init__(
360
362
Additional OpenAPI extensions as a dictionary.
361
363
deprecated: bool
362
364
Whether or not to mark this route as deprecated in the OpenAPI schema
365
+ custom_response_validation_http_code: int | HTTPStatus | None, optional
366
+ Whether to have custom http status code for this route if response validation fails
363
367
middlewares: list[Callable[..., Response]] | None
364
368
The list of route middlewares to be called in order.
365
369
"""
366
370
self .method = method .upper ()
367
- self .path = "/" if path .strip () == "" else path
371
+ self .path = path if path .strip () else "/"
368
372
369
373
# OpenAPI spec only understands paths with { }. So we'll have to convert Powertools' < >.
370
374
# https://swagger.io/specification/#path-templating
@@ -397,6 +401,8 @@ def __init__(
397
401
# _body_field is used to cache the dependant model for the body field
398
402
self ._body_field : ModelField | None = None
399
403
404
+ self .custom_response_validation_http_code = custom_response_validation_http_code
405
+
400
406
def __call__ (
401
407
self ,
402
408
router_middlewares : list [Callable ],
@@ -565,6 +571,16 @@ def _get_openapi_path(
565
571
},
566
572
}
567
573
574
+ # Add custom response validation response, if exists
575
+ if self .custom_response_validation_http_code :
576
+ http_code = self .custom_response_validation_http_code .value
577
+ operation_responses [http_code ] = {
578
+ "description" : "Response Validation Error" ,
579
+ "content" : {"application/json" : {"schema" : {"$ref" : f"{ COMPONENT_REF_PREFIX } ResponseValidationError" }}},
580
+ }
581
+ # Add model definition
582
+ definitions ["ResponseValidationError" ] = response_validation_error_response_definition
583
+
568
584
# Add the response to the OpenAPI operation
569
585
if self .responses :
570
586
for status_code in list (self .responses ):
@@ -942,6 +958,7 @@ def route(
942
958
security : list [dict [str , list [str ]]] | None = None ,
943
959
openapi_extensions : dict [str , Any ] | None = None ,
944
960
deprecated : bool = False ,
961
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
945
962
middlewares : list [Callable [..., Any ]] | None = None ,
946
963
) -> Callable [[AnyCallableT ], AnyCallableT ]:
947
964
raise NotImplementedError ()
@@ -1003,6 +1020,7 @@ def get(
1003
1020
security : list [dict [str , list [str ]]] | None = None ,
1004
1021
openapi_extensions : dict [str , Any ] | None = None ,
1005
1022
deprecated : bool = False ,
1023
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1006
1024
middlewares : list [Callable [..., Any ]] | None = None ,
1007
1025
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1008
1026
"""Get route decorator with GET `method`
@@ -1043,6 +1061,7 @@ def lambda_handler(event, context):
1043
1061
security ,
1044
1062
openapi_extensions ,
1045
1063
deprecated ,
1064
+ custom_response_validation_http_code ,
1046
1065
middlewares ,
1047
1066
)
1048
1067
@@ -1062,6 +1081,7 @@ def post(
1062
1081
security : list [dict [str , list [str ]]] | None = None ,
1063
1082
openapi_extensions : dict [str , Any ] | None = None ,
1064
1083
deprecated : bool = False ,
1084
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1065
1085
middlewares : list [Callable [..., Any ]] | None = None ,
1066
1086
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1067
1087
"""Post route decorator with POST `method`
@@ -1103,6 +1123,7 @@ def lambda_handler(event, context):
1103
1123
security ,
1104
1124
openapi_extensions ,
1105
1125
deprecated ,
1126
+ custom_response_validation_http_code ,
1106
1127
middlewares ,
1107
1128
)
1108
1129
@@ -1122,6 +1143,7 @@ def put(
1122
1143
security : list [dict [str , list [str ]]] | None = None ,
1123
1144
openapi_extensions : dict [str , Any ] | None = None ,
1124
1145
deprecated : bool = False ,
1146
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1125
1147
middlewares : list [Callable [..., Any ]] | None = None ,
1126
1148
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1127
1149
"""Put route decorator with PUT `method`
@@ -1163,6 +1185,7 @@ def lambda_handler(event, context):
1163
1185
security ,
1164
1186
openapi_extensions ,
1165
1187
deprecated ,
1188
+ custom_response_validation_http_code ,
1166
1189
middlewares ,
1167
1190
)
1168
1191
@@ -1182,6 +1205,7 @@ def delete(
1182
1205
security : list [dict [str , list [str ]]] | None = None ,
1183
1206
openapi_extensions : dict [str , Any ] | None = None ,
1184
1207
deprecated : bool = False ,
1208
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1185
1209
middlewares : list [Callable [..., Any ]] | None = None ,
1186
1210
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1187
1211
"""Delete route decorator with DELETE `method`
@@ -1222,6 +1246,7 @@ def lambda_handler(event, context):
1222
1246
security ,
1223
1247
openapi_extensions ,
1224
1248
deprecated ,
1249
+ custom_response_validation_http_code ,
1225
1250
middlewares ,
1226
1251
)
1227
1252
@@ -1241,6 +1266,7 @@ def patch(
1241
1266
security : list [dict [str , list [str ]]] | None = None ,
1242
1267
openapi_extensions : dict [str , Any ] | None = None ,
1243
1268
deprecated : bool = False ,
1269
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1244
1270
middlewares : list [Callable ] | None = None ,
1245
1271
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1246
1272
"""Patch route decorator with PATCH `method`
@@ -1284,6 +1310,7 @@ def lambda_handler(event, context):
1284
1310
security ,
1285
1311
openapi_extensions ,
1286
1312
deprecated ,
1313
+ custom_response_validation_http_code ,
1287
1314
middlewares ,
1288
1315
)
1289
1316
@@ -1303,6 +1330,7 @@ def head(
1303
1330
security : list [dict [str , list [str ]]] | None = None ,
1304
1331
openapi_extensions : dict [str , Any ] | None = None ,
1305
1332
deprecated : bool = False ,
1333
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
1306
1334
middlewares : list [Callable ] | None = None ,
1307
1335
) -> Callable [[AnyCallableT ], AnyCallableT ]:
1308
1336
"""Head route decorator with HEAD `method`
@@ -1345,6 +1373,7 @@ def lambda_handler(event, context):
1345
1373
security ,
1346
1374
openapi_extensions ,
1347
1375
deprecated ,
1376
+ custom_response_validation_http_code ,
1348
1377
middlewares ,
1349
1378
)
1350
1379
@@ -1571,6 +1600,7 @@ def _validate_response_validation_error_http_code(
1571
1600
response_validation_error_http_code : HTTPStatus | int | None ,
1572
1601
enable_validation : bool ,
1573
1602
) -> HTTPStatus :
1603
+
1574
1604
if response_validation_error_http_code and not enable_validation :
1575
1605
msg = "'response_validation_error_http_code' cannot be set when enable_validation is False."
1576
1606
raise ValueError (msg )
@@ -1588,6 +1618,33 @@ def _validate_response_validation_error_http_code(
1588
1618
1589
1619
return response_validation_error_http_code or HTTPStatus .UNPROCESSABLE_ENTITY
1590
1620
1621
+ def _add_resolver_response_validation_error_response_to_route (
1622
+ self ,
1623
+ route_openapi_path : tuple [dict [str , Any ], dict [str , Any ]],
1624
+ ) -> tuple [dict [str , Any ], dict [str , Any ]]:
1625
+ """Adds resolver response validation error response to route's operations."""
1626
+ path , path_definitions = route_openapi_path
1627
+ if self ._has_response_validation_error and "ResponseValidationError" not in path_definitions :
1628
+ response_validation_error_response = {
1629
+ "description" : "Response Validation Error" ,
1630
+ "content" : {
1631
+ "application/json" : {
1632
+ "schema" : {"$ref" : f"{ COMPONENT_REF_PREFIX } ResponseValidationError" },
1633
+ },
1634
+ },
1635
+ }
1636
+ http_code = self ._response_validation_error_http_code .value
1637
+ for operation in path .values ():
1638
+ operation ["responses" ][http_code ] = response_validation_error_response
1639
+ return path , path_definitions
1640
+
1641
+ def _generate_schemas (self , definitions : dict [str , dict [str , Any ]]) -> dict [str , dict [str , Any ]]:
1642
+ schemas = {k : definitions [k ] for k in sorted (definitions )}
1643
+ # add response validation error definition
1644
+ if self ._response_validation_error_http_code :
1645
+ schemas .setdefault ("ResponseValidationError" , response_validation_error_response_definition )
1646
+ return schemas
1647
+
1591
1648
def get_openapi_schema (
1592
1649
self ,
1593
1650
* ,
@@ -1739,14 +1796,14 @@ def get_openapi_schema(
1739
1796
field_mapping = field_mapping ,
1740
1797
)
1741
1798
if result :
1742
- path , path_definitions = result
1799
+ path , path_definitions = self . _add_resolver_response_validation_error_response_to_route ( result )
1743
1800
if path :
1744
1801
paths .setdefault (route .openapi_path , {}).update (path )
1745
1802
if path_definitions :
1746
1803
definitions .update (path_definitions )
1747
1804
1748
1805
if definitions :
1749
- components ["schemas" ] = { k : definitions [ k ] for k in sorted (definitions )}
1806
+ components ["schemas" ] = self . _generate_schemas (definitions )
1750
1807
if security_schemes :
1751
1808
components ["securitySchemes" ] = security_schemes
1752
1809
if components :
@@ -2108,6 +2165,29 @@ def swagger_handler():
2108
2165
body = body ,
2109
2166
)
2110
2167
2168
+ def _validate_route_response_validation_error_http_code (
2169
+ self ,
2170
+ custom_response_validation_http_code : int | HTTPStatus | None ,
2171
+ ) -> HTTPStatus | None :
2172
+ if custom_response_validation_http_code and not self ._enable_validation :
2173
+ msg = (
2174
+ "'custom_response_validation_http_code' cannot be set for route when enable_validation is False "
2175
+ "on resolver."
2176
+ )
2177
+ raise ValueError (msg )
2178
+
2179
+ if (
2180
+ not isinstance (custom_response_validation_http_code , HTTPStatus )
2181
+ and custom_response_validation_http_code is not None
2182
+ ):
2183
+ try :
2184
+ custom_response_validation_http_code = HTTPStatus (custom_response_validation_http_code )
2185
+ except ValueError :
2186
+ msg = f"'{ custom_response_validation_http_code } ' must be an integer representing an HTTP status code or an enum of type HTTPStatus." # noqa: E501
2187
+ raise ValueError (msg ) from None
2188
+
2189
+ return custom_response_validation_http_code
2190
+
2111
2191
def route (
2112
2192
self ,
2113
2193
rule : str ,
@@ -2125,10 +2205,15 @@ def route(
2125
2205
security : list [dict [str , list [str ]]] | None = None ,
2126
2206
openapi_extensions : dict [str , Any ] | None = None ,
2127
2207
deprecated : bool = False ,
2208
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2128
2209
middlewares : list [Callable [..., Any ]] | None = None ,
2129
2210
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2130
2211
"""Route decorator includes parameter `method`"""
2131
2212
2213
+ custom_response_validation_http_code = self ._validate_route_response_validation_error_http_code (
2214
+ custom_response_validation_http_code ,
2215
+ )
2216
+
2132
2217
def register_resolver (func : AnyCallableT ) -> AnyCallableT :
2133
2218
methods = (method ,) if isinstance (method , str ) else method
2134
2219
logger .debug (f"Adding route using rule { rule } and methods: { ',' .join (m .upper () for m in methods )} " )
@@ -2154,6 +2239,7 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT:
2154
2239
security ,
2155
2240
openapi_extensions ,
2156
2241
deprecated ,
2242
+ custom_response_validation_http_code ,
2157
2243
middlewares ,
2158
2244
)
2159
2245
@@ -2523,15 +2609,17 @@ def _call_exception_handler(self, exp: Exception, route: Route) -> ResponseBuild
2523
2609
)
2524
2610
2525
2611
# OpenAPIValidationMiddleware will only raise ResponseValidationError when
2526
- # 'self._response_validation_error_http_code' is not None
2612
+ # 'self._response_validation_error_http_code' is not None or
2613
+ # when route has custom_response_validation_http_code
2527
2614
if isinstance (exp , ResponseValidationError ):
2528
- http_code = self ._response_validation_error_http_code
2615
+ # route validation must take precedence over app validation
2616
+ http_code = route .custom_response_validation_http_code or self ._response_validation_error_http_code
2529
2617
errors = [{"loc" : e ["loc" ], "type" : e ["type" ]} for e in exp .errors ()]
2530
2618
return self ._response_builder_class (
2531
2619
response = Response (
2532
2620
status_code = http_code .value ,
2533
2621
content_type = content_types .APPLICATION_JSON ,
2534
- body = {"statusCode" : self . _response_validation_error_http_code , "detail" : errors },
2622
+ body = {"statusCode" : http_code , "detail" : errors },
2535
2623
),
2536
2624
serializer = self ._serializer ,
2537
2625
route = route ,
@@ -2682,6 +2770,7 @@ def route(
2682
2770
security : list [dict [str , list [str ]]] | None = None ,
2683
2771
openapi_extensions : dict [str , Any ] | None = None ,
2684
2772
deprecated : bool = False ,
2773
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2685
2774
middlewares : list [Callable [..., Any ]] | None = None ,
2686
2775
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2687
2776
def register_route (func : AnyCallableT ) -> AnyCallableT :
@@ -2708,6 +2797,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT:
2708
2797
frozen_security ,
2709
2798
frozen_openapi_extensions ,
2710
2799
deprecated ,
2800
+ custom_response_validation_http_code ,
2711
2801
)
2712
2802
2713
2803
# Collate Middleware for routes
@@ -2794,6 +2884,7 @@ def route(
2794
2884
security : list [dict [str , list [str ]]] | None = None ,
2795
2885
openapi_extensions : dict [str , Any ] | None = None ,
2796
2886
deprecated : bool = False ,
2887
+ custom_response_validation_http_code : int | HTTPStatus | None = None ,
2797
2888
middlewares : list [Callable [..., Any ]] | None = None ,
2798
2889
) -> Callable [[AnyCallableT ], AnyCallableT ]:
2799
2890
# NOTE: see #1552 for more context.
@@ -2813,6 +2904,7 @@ def route(
2813
2904
security ,
2814
2905
openapi_extensions ,
2815
2906
deprecated ,
2907
+ custom_response_validation_http_code ,
2816
2908
middlewares ,
2817
2909
)
2818
2910
0 commit comments