Skip to content

Commit fe2cf13

Browse files
author
Tom McCarthy
authored
fix(apigateway): allow list of HTTP methods in route method (#838)
1 parent 566043a commit fe2cf13

File tree

3 files changed

+145
-43
lines changed

3 files changed

+145
-43
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from enum import Enum
1111
from functools import partial
1212
from http import HTTPStatus
13-
from typing import Any, Callable, Dict, List, Optional, Set, Union
13+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
1414

1515
from aws_lambda_powertools.event_handler import content_types
1616
from aws_lambda_powertools.event_handler.exceptions import ServiceError
@@ -453,27 +453,30 @@ def __init__(
453453
def route(
454454
self,
455455
rule: str,
456-
method: str,
456+
method: Union[str, Union[List[str], Tuple[str]]],
457457
cors: Optional[bool] = None,
458458
compress: bool = False,
459459
cache_control: Optional[str] = None,
460460
):
461461
"""Route decorator includes parameter `method`"""
462462

463463
def register_resolver(func: Callable):
464-
logger.debug(f"Adding route using rule {rule} and method {method.upper()}")
464+
methods = (method,) if isinstance(method, str) else method
465+
logger.debug(f"Adding route using rule {rule} and methods: {','.join((m.upper() for m in methods))}")
465466
if cors is None:
466467
cors_enabled = self._cors_enabled
467468
else:
468469
cors_enabled = cors
469-
self._routes.append(Route(method, self._compile_regex(rule), func, cors_enabled, compress, cache_control))
470-
route_key = method + rule
471-
if route_key in self._route_keys:
472-
warnings.warn(f"A route like this was already registered. method: '{method}' rule: '{rule}'")
473-
self._route_keys.append(route_key)
474-
if cors_enabled:
475-
logger.debug(f"Registering method {method.upper()} to Allow Methods in CORS")
476-
self._cors_methods.add(method.upper())
470+
471+
for item in methods:
472+
self._routes.append(Route(item, self._compile_regex(rule), func, cors_enabled, compress, cache_control))
473+
route_key = item + rule
474+
if route_key in self._route_keys:
475+
warnings.warn(f"A route like this was already registered. method: '{item}' rule: '{rule}'")
476+
self._route_keys.append(route_key)
477+
if cors_enabled:
478+
logger.debug(f"Registering method {item.upper()} to Allow Methods in CORS")
479+
self._cors_methods.add(item.upper())
477480
return func
478481

479482
return register_resolver
@@ -679,14 +682,14 @@ def __init__(self):
679682
def route(
680683
self,
681684
rule: str,
682-
method: Union[str, List[str]],
685+
method: Union[str, Union[List[str], Tuple[str]]],
683686
cors: Optional[bool] = None,
684687
compress: bool = False,
685688
cache_control: Optional[str] = None,
686689
):
687690
def register_route(func: Callable):
688-
methods = method if isinstance(method, list) else [method]
689-
for item in methods:
690-
self._routes[(rule, item, cors, compress, cache_control)] = func
691+
# Convert methods to tuple. It needs to be hashable as its part of the self._routes dict key
692+
methods = (method,) if isinstance(method, str) else tuple(method)
693+
self._routes[(rule, methods, cors, compress, cache_control)] = func
691694

692695
return register_route

docs/core/event_handler/api_gateway.md

+91-28
Original file line numberDiff line numberDiff line change
@@ -42,45 +42,27 @@ This is the sample infrastructure for API Gateway we are using for the examples
4242
Timeout: 5
4343
Runtime: python3.8
4444
Tracing: Active
45-
Environment:
45+
Environment:
4646
Variables:
4747
LOG_LEVEL: INFO
4848
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
4949
POWERTOOLS_LOGGER_LOG_EVENT: true
5050
POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication
51-
POWERTOOLS_SERVICE_NAME: hello
51+
POWERTOOLS_SERVICE_NAME: my_api-service
5252

5353
Resources:
54-
HelloWorldFunction:
54+
ApiFunction:
5555
Type: AWS::Serverless::Function
5656
Properties:
5757
Handler: app.lambda_handler
58-
CodeUri: hello_world
59-
Description: Hello World function
58+
CodeUri: api_handler/
59+
Description: API handler function
6060
Events:
61-
HelloUniverse:
62-
Type: Api
63-
Properties:
64-
Path: /hello
65-
Method: GET
66-
HelloYou:
67-
Type: Api
68-
Properties:
69-
Path: /hello/{name} # see Dynamic routes section
70-
Method: GET
71-
CustomMessage:
72-
Type: Api
73-
Properties:
74-
Path: /{message}/{name} # see Dynamic routes section
75-
Method: GET
76-
77-
Outputs:
78-
HelloWorldApigwURL:
79-
Description: "API Gateway endpoint URL for Prod environment for Hello World Function"
80-
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello"
81-
HelloWorldFunction:
82-
Description: "Hello World Lambda Function ARN"
83-
Value: !GetAtt HelloWorldFunction.Arn
61+
ApiEvent:
62+
Type: Api
63+
Properties:
64+
Path: /{proxy+} # Send requests on any path to the lambda function
65+
Method: ANY # Send requests using any http method to the lambda function
8466
```
8567

8668
### API Gateway decorator
@@ -360,6 +342,87 @@ You can also combine nested paths with greedy regex to catch in between routes.
360342
...
361343
}
362344
```
345+
### HTTP Methods
346+
You can use named decorators to specify the HTTP method that should be handled in your functions. As well as the
347+
`get` method already shown above, you can use `post`, `put`, `patch`, `delete`, and `patch`.
348+
349+
=== "app.py"
350+
351+
```python hl_lines="9-10"
352+
from aws_lambda_powertools import Logger, Tracer
353+
from aws_lambda_powertools.logging import correlation_paths
354+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
355+
356+
tracer = Tracer()
357+
logger = Logger()
358+
app = ApiGatewayResolver()
359+
360+
# Only POST HTTP requests to the path /hello will route to this function
361+
@app.post("/hello")
362+
@tracer.capture_method
363+
def get_hello_you():
364+
name = app.current_event.json_body.get("name")
365+
return {"message": f"hello {name}"}
366+
367+
# You can continue to use other utilities just as before
368+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
369+
@tracer.capture_lambda_handler
370+
def lambda_handler(event, context):
371+
return app.resolve(event, context)
372+
```
373+
374+
=== "sample_request.json"
375+
376+
```json
377+
{
378+
"resource": "/hello/{name}",
379+
"path": "/hello/lessa",
380+
"httpMethod": "GET",
381+
...
382+
}
383+
```
384+
385+
If you need to accept multiple HTTP methods in a single function, you can use the `route` method and pass a list of
386+
HTTP methods.
387+
388+
=== "app.py"
389+
390+
```python hl_lines="9-10"
391+
from aws_lambda_powertools import Logger, Tracer
392+
from aws_lambda_powertools.logging import correlation_paths
393+
from aws_lambda_powertools.event_handler.api_gateway import ApiGatewayResolver
394+
395+
tracer = Tracer()
396+
logger = Logger()
397+
app = ApiGatewayResolver()
398+
399+
# PUT and POST HTTP requests to the path /hello will route to this function
400+
@app.route("/hello", method=["PUT", "POST"])
401+
@tracer.capture_method
402+
def get_hello_you():
403+
name = app.current_event.json_body.get("name")
404+
return {"message": f"hello {name}"}
405+
406+
# You can continue to use other utilities just as before
407+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
408+
@tracer.capture_lambda_handler
409+
def lambda_handler(event, context):
410+
return app.resolve(event, context)
411+
```
412+
413+
=== "sample_request.json"
414+
415+
```json
416+
{
417+
"resource": "/hello/{name}",
418+
"path": "/hello/lessa",
419+
"httpMethod": "GET",
420+
...
421+
}
422+
```
423+
424+
!!! note "It is usually better to have separate functions for each HTTP method, as the functionality tends to differ
425+
depending on which method is used."
363426

364427
### Accessing request details
365428

tests/functional/event_handler/test_api_gateway.py

+36
Original file line numberDiff line numberDiff line change
@@ -1021,3 +1021,39 @@ def get_func_another_duplicate():
10211021
# THEN only execute the first registered route
10221022
# AND print warnings
10231023
assert result["statusCode"] == 200
1024+
1025+
1026+
def test_route_multiple_methods():
1027+
# GIVEN a function with http methods passed as a list
1028+
app = ApiGatewayResolver()
1029+
req = "foo"
1030+
get_event = deepcopy(LOAD_GW_EVENT)
1031+
get_event["resource"] = "/accounts/{account_id}"
1032+
get_event["path"] = f"/accounts/{req}"
1033+
1034+
post_event = deepcopy(get_event)
1035+
post_event["httpMethod"] = "POST"
1036+
1037+
put_event = deepcopy(get_event)
1038+
put_event["httpMethod"] = "PUT"
1039+
1040+
lambda_context = {}
1041+
1042+
@app.route(rule="/accounts/<account_id>", method=["GET", "POST"])
1043+
def foo(account_id):
1044+
assert app.lambda_context == lambda_context
1045+
assert account_id == f"{req}"
1046+
return {}
1047+
1048+
# WHEN calling the event handler with the supplied methods
1049+
get_result = app(get_event, lambda_context)
1050+
post_result = app(post_event, lambda_context)
1051+
put_result = app(put_event, lambda_context)
1052+
1053+
# THEN events are processed correctly
1054+
assert get_result["statusCode"] == 200
1055+
assert get_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON
1056+
assert post_result["statusCode"] == 200
1057+
assert post_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON
1058+
assert put_result["statusCode"] == 404
1059+
assert put_result["headers"]["Content-Type"] == content_types.APPLICATION_JSON

0 commit comments

Comments
 (0)