Skip to content

Commit cf87113

Browse files
bchamppreedham-aws
andauthored
feat: add support for ResponseTransferMode on Api resource (#3881)
Co-authored-by: Reed Hamilton <reedham@amazon.com>
1 parent 62efdc3 commit cf87113

File tree

11 files changed

+584
-8
lines changed

11 files changed

+584
-8
lines changed

samtranslator/internal/schema_source/aws_serverless_function.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ class ApiEventProperties(BaseModel):
308308
"TimeoutInMillis",
309309
["AWS::ApiGateway::Method.Integration", "TimeoutInMillis"],
310310
)
311+
ResponseTransferMode: Optional[PassThroughProp] = apieventproperties("ResponseTransferMode")
311312

312313

313314
class ApiEvent(BaseModel):

samtranslator/internal/schema_source/sam-docs.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@
150150
"RequestModel": "Request model to use for this specific Api\\$1Path\\$1Method. This should reference the name of a model specified in the `Models` section of an [AWS::Serverless::Api](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html) resource. \n*Type*: [RequestModel](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestmodel.html) \n*Required*: No \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.",
151151
"RequestParameters": "Request parameters configuration for this specific Api\\$1Path\\$1Method. All parameter names must start with `method.request` and must be limited to `method.request.header`, `method.request.querystring`, or `method.request.path`. \nA list can contain both parameter name strings and [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) objects. For strings, the `Required` and `Caching` properties will default to `false`. \n*Type*: List of [ String \\$1 [RequestParameter](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-requestparameter.html) ] \n*Required*: No \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.",
152152
"RestApiId": "Identifier of a RestApi resource, which must contain an operation with the given path and method. Typically, this is set to reference an [AWS::Serverless::Api](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html) resource defined in this template. \nIf you don't define this property, AWS SAM creates a default [AWS::Serverless::Api](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html) resource using a generated `OpenApi` document. That resource contains a union of all paths and methods defined by `Api` events in the same template that do not specify a `RestApiId`. \nThis cannot reference an [AWS::Serverless::Api](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html) resource defined in another template. \n*Type*: String \n*Required*: No \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.",
153-
"TimeoutInMillis": "Custom timeout between 50 and 29,000 milliseconds. \nWhen you specify this property, AWS SAM modifies your OpenAPI definition. The OpenAPI definition must be specified inline using the `DefinitionBody` property. \n*Type*: Integer \n*Required*: No \n*Default*: 29,000 milliseconds or 29 seconds \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent."
153+
"TimeoutInMillis": "Custom timeout between 50 and 29,000 milliseconds. \nWhen you specify this property, AWS SAM modifies your OpenAPI definition. The OpenAPI definition must be specified inline using the `DefinitionBody` property. \n*Type*: Integer \n*Required*: No \n*Default*: 29,000 milliseconds or 29 seconds \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.",
154+
"ResponseTransferMode": "The response transfer mode for the Lambda function integration. Set to `RESPONSE_STREAM` to enable Lambda response streaming through API Gateway, allowing the function to stream responses back to clients. When set to `RESPONSE_STREAM`, API Gateway uses the Lambda InvokeWithResponseStreaming API. \n*Type*: String \n*Required*: No \n*Valid values*: `BUFFERED` \\| `RESPONSE_STREAM` \n*CloudFormation compatibility*: This property is passed directly to the [`ResponseTransferMode`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-apigateway-method-integration.html#cfn-apigateway-method-integration-responsetransfermode) property of an `AWS::ApiGateway::Method Integration`\\."
154155
},
155156
"sam-property-function-apifunctionauth": {
156157
"ApiKeyRequired": "Requires an API key for this API, path, and method. \n*Type*: Boolean \n*Required*: No \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent.",
@@ -886,4 +887,4 @@
886887
"UseAliasAsEventTarget": "Indicate whether or not to pass the alias, created by using the `AutoPublishAlias` property, to the events source's target defined with [Events](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/#sam-statemachine-events.html#sam-statemachine-events). \nSpecify `True` to use the alias as the events' target. \n*Type*: Boolean \n*Required*: No \n*Default*: `False` \n*CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an CloudFormation equivalent."
887888
}
888889
}
889-
}
890+
}

samtranslator/model/eventsources/push.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,7 @@ class Api(PushEventSource):
689689
"RequestModel": PropertyType(False, IS_DICT),
690690
"RequestParameters": PropertyType(False, IS_LIST),
691691
"TimeoutInMillis": PropertyType(False, IS_INT),
692+
"ResponseTransferMode": PropertyType(False, IS_STR),
692693
}
693694

694695
Path: str
@@ -699,6 +700,7 @@ class Api(PushEventSource):
699700
RequestModel: Optional[Dict[str, Any]]
700701
RequestParameters: Optional[List[Any]]
701702
TimeoutInMillis: Optional[PassThrough]
703+
ResponseTransferMode: Optional[PassThrough]
702704

703705
def resources_to_link(self, resources: Dict[str, Any]) -> Dict[str, Any]:
704706
"""
@@ -863,7 +865,7 @@ def _add_swagger_integration( # type: ignore[no-untyped-def] # noqa: PLR0912, P
863865
swagger_body = SwaggerEditor.gen_skeleton()
864866

865867
partition = ArnGenerator.get_partition_name()
866-
uri = _build_apigw_integration_uri(function, partition) # type: ignore[no-untyped-call]
868+
uri = _build_apigw_integration_uri(function, partition, self.ResponseTransferMode) # type: ignore[no-untyped-call]
867869

868870
editor = SwaggerEditor(swagger_body)
869871

@@ -882,7 +884,15 @@ def _add_swagger_integration( # type: ignore[no-untyped-def] # noqa: PLR0912, P
882884
sam_expect(method_auth, self.relative_id, "Auth", is_sam_event=True).to_be_a_map()
883885
api_auth = api.get("Auth") or Py27Dict()
884886
sam_expect(api_auth, api_id, "Auth").to_be_a_map()
885-
editor.add_lambda_integration(self.Path, self.Method, uri, method_auth, api_auth, condition=condition)
887+
editor.add_lambda_integration(
888+
self.Path,
889+
self.Method,
890+
uri,
891+
method_auth,
892+
api_auth,
893+
condition=condition,
894+
invoke_mode=self.ResponseTransferMode,
895+
)
886896

887897
# self.Stage is not None as it is set in _get_permissions()
888898
# before calling this method.
@@ -1550,14 +1560,23 @@ def _add_auth_to_openapi_integration(
15501560
editor.add_auth_to_method(api=api, path=self._path, method_name=self._method, auth=self.Auth) # type: ignore[no-untyped-call]
15511561

15521562

1553-
def _build_apigw_integration_uri(function, partition): # type: ignore[no-untyped-def]
1563+
def _build_apigw_integration_uri(function, partition, response_transfer_mode=None): # type: ignore[no-untyped-def]
15541564
function_arn = function.get_runtime_attr("arn")
1565+
# Use response-streaming-invocations path when ResponseTransferMode is RESPONSE_STREAM
1566+
# See: https://aws.amazon.com/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/
1567+
if response_transfer_mode == "RESPONSE_STREAM":
1568+
api_version = "2021-11-15"
1569+
invocation_path = "/response-streaming-invocations"
1570+
else:
1571+
api_version = "2015-03-31"
1572+
invocation_path = "/invocations"
1573+
15551574
arn = (
15561575
"arn:"
15571576
+ partition
1558-
+ ":apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/"
1577+
+ f":apigateway:${{AWS::Region}}:lambda:path/{api_version}/functions/"
15591578
+ make_shorthand(function_arn)
1560-
+ "/invocations"
1579+
+ invocation_path
15611580
)
15621581
# function_arn is always of the form {"Fn::GetAtt": ["<function_logical_id>", "Arn"]}.
15631582
# We only want to check if the function logical id is a Py27UniStr instance.

samtranslator/open_api/open_api.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,14 @@ def is_integration_function_logical_id_match(self, path_name, method_name, logic
114114
return True
115115

116116
def add_lambda_integration( # type: ignore[no-untyped-def] # noqa: PLR0913
117-
self, path, method, integration_uri, method_auth_config=None, api_auth_config=None, condition=None
117+
self,
118+
path,
119+
method,
120+
integration_uri,
121+
method_auth_config=None,
122+
api_auth_config=None,
123+
condition=None,
124+
invoke_mode=None,
118125
):
119126
"""
120127
Adds aws_proxy APIGW integration to the given path+method.
@@ -147,6 +154,9 @@ def add_lambda_integration( # type: ignore[no-untyped-def] # noqa: PLR0913
147154
path_item[method][self._X_APIGW_INTEGRATION]["payloadFormatVersion"] = "2.0"
148155
path_item[method][self._X_APIGW_INTEGRATION]["uri"] = integration_uri
149156

157+
if invoke_mode:
158+
path_item[method][self._X_APIGW_INTEGRATION]["invokeMode"] = invoke_mode
159+
150160
if path == self._DEFAULT_PATH and method == self._X_ANY_METHOD:
151161
path_item[method]["isDefaultRoute"] = True
152162

samtranslator/schema/schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360664,6 +360664,15 @@
360664360664
"title": "RequestParameters",
360665360665
"type": "array"
360666360666
},
360667+
"ResponseTransferMode": {
360668+
"allOf": [
360669+
{
360670+
"$ref": "#/definitions/PassThroughProp"
360671+
}
360672+
],
360673+
"markdownDescription": "The response transfer mode for the Lambda function integration. Set to `RESPONSE_STREAM` to enable Lambda response streaming through API Gateway, allowing the function to stream responses back to clients. When set to `RESPONSE_STREAM`, API Gateway uses the Lambda InvokeWithResponseStreaming API. \n*Type*: String \n*Required*: No \n*Valid values*: `BUFFERED` \\| `RESPONSE_STREAM` \n*CloudFormation compatibility*: This property is passed directly to the [`ResponseTransferMode`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-apigateway-method-integration.html#cfn-apigateway-method-integration-responsetransfermode) property of an `AWS::ApiGateway::Method Integration`\\.",
360674+
"title": "ResponseTransferMode"
360675+
},
360667360676
"RestApiId": {
360668360677
"anyOf": [
360669360678
{

samtranslator/swagger/swagger.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ def add_lambda_integration( # noqa: PLR0913
125125
method_auth_config: Dict[str, Any],
126126
api_auth_config: Dict[str, Any],
127127
condition: Optional[str] = None,
128+
invoke_mode: Optional[Any] = None,
128129
) -> None:
129130
"""
130131
Adds aws_proxy APIGW integration to the given path+method.
@@ -150,6 +151,10 @@ def add_lambda_integration( # noqa: PLR0913
150151
path_item[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST"
151152
path_item[method][self._X_APIGW_INTEGRATION]["uri"] = _integration_uri
152153

154+
# When using RESPONSE_STREAM invoke mode, set responseTransferMode to STREAM
155+
if invoke_mode == "RESPONSE_STREAM":
156+
path_item[method][self._X_APIGW_INTEGRATION]["responseTransferMode"] = "STREAM"
157+
153158
if (
154159
method_auth_config.get("Authorizer") == "AWS_IAM"
155160
or api_auth_config.get("DefaultAuthorizer") == "AWS_IAM"

schema_source/sam.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6103,6 +6103,15 @@
61036103
"title": "RequestParameters",
61046104
"type": "array"
61056105
},
6106+
"ResponseTransferMode": {
6107+
"allOf": [
6108+
{
6109+
"$ref": "#/definitions/PassThroughProp"
6110+
}
6111+
],
6112+
"markdownDescription": "The response transfer mode for the Lambda function integration. Set to `RESPONSE_STREAM` to enable Lambda response streaming through API Gateway, allowing the function to stream responses back to clients. When set to `RESPONSE_STREAM`, API Gateway uses the Lambda InvokeWithResponseStreaming API. \n*Type*: String \n*Required*: No \n*Valid values*: `BUFFERED` \\| `RESPONSE_STREAM` \n*CloudFormation compatibility*: This property is passed directly to the [`ResponseTransferMode`](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-apigateway-method-integration.html#cfn-apigateway-method-integration-responsetransfermode) property of an `AWS::ApiGateway::Method Integration`\\.",
6113+
"title": "ResponseTransferMode"
6114+
},
61066115
"RestApiId": {
61076116
"anyOf": [
61086117
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Resources:
2+
MyFunction:
3+
Type: AWS::Serverless::Function
4+
Properties:
5+
CodeUri: s3://sam-demo-bucket/stream.zip
6+
Handler: index.handler
7+
Runtime: nodejs20.x
8+
Events:
9+
StreamApi:
10+
Type: Api
11+
Properties:
12+
Path: /stream
13+
Method: post
14+
ResponseTransferMode: RESPONSE_STREAM
15+
BufferedApi:
16+
Type: Api
17+
Properties:
18+
Path: /buffered
19+
Method: get
20+
ResponseTransferMode: BUFFERED
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
{
2+
"Resources": {
3+
"MyFunction": {
4+
"Properties": {
5+
"Code": {
6+
"S3Bucket": "sam-demo-bucket",
7+
"S3Key": "stream.zip"
8+
},
9+
"Handler": "index.handler",
10+
"Role": {
11+
"Fn::GetAtt": [
12+
"MyFunctionRole",
13+
"Arn"
14+
]
15+
},
16+
"Runtime": "nodejs20.x",
17+
"Tags": [
18+
{
19+
"Key": "lambda:createdBy",
20+
"Value": "SAM"
21+
}
22+
]
23+
},
24+
"Type": "AWS::Lambda::Function"
25+
},
26+
"MyFunctionBufferedApiPermissionProd": {
27+
"Properties": {
28+
"Action": "lambda:InvokeFunction",
29+
"FunctionName": {
30+
"Ref": "MyFunction"
31+
},
32+
"Principal": "apigateway.amazonaws.com",
33+
"SourceArn": {
34+
"Fn::Sub": [
35+
"arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/buffered",
36+
{
37+
"__ApiId__": {
38+
"Ref": "ServerlessRestApi"
39+
},
40+
"__Stage__": "*"
41+
}
42+
]
43+
}
44+
},
45+
"Type": "AWS::Lambda::Permission"
46+
},
47+
"MyFunctionRole": {
48+
"Properties": {
49+
"AssumeRolePolicyDocument": {
50+
"Statement": [
51+
{
52+
"Action": [
53+
"sts:AssumeRole"
54+
],
55+
"Effect": "Allow",
56+
"Principal": {
57+
"Service": [
58+
"lambda.amazonaws.com"
59+
]
60+
}
61+
}
62+
],
63+
"Version": "2012-10-17"
64+
},
65+
"ManagedPolicyArns": [
66+
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
67+
],
68+
"Tags": [
69+
{
70+
"Key": "lambda:createdBy",
71+
"Value": "SAM"
72+
}
73+
]
74+
},
75+
"Type": "AWS::IAM::Role"
76+
},
77+
"MyFunctionStreamApiPermissionProd": {
78+
"Properties": {
79+
"Action": "lambda:InvokeFunction",
80+
"FunctionName": {
81+
"Ref": "MyFunction"
82+
},
83+
"Principal": "apigateway.amazonaws.com",
84+
"SourceArn": {
85+
"Fn::Sub": [
86+
"arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/stream",
87+
{
88+
"__ApiId__": {
89+
"Ref": "ServerlessRestApi"
90+
},
91+
"__Stage__": "*"
92+
}
93+
]
94+
}
95+
},
96+
"Type": "AWS::Lambda::Permission"
97+
},
98+
"ServerlessRestApi": {
99+
"Properties": {
100+
"Body": {
101+
"info": {
102+
"title": {
103+
"Ref": "AWS::StackName"
104+
},
105+
"version": "1.0"
106+
},
107+
"paths": {
108+
"/buffered": {
109+
"get": {
110+
"responses": {},
111+
"x-amazon-apigateway-integration": {
112+
"httpMethod": "POST",
113+
"type": "aws_proxy",
114+
"uri": {
115+
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations"
116+
}
117+
}
118+
}
119+
},
120+
"/stream": {
121+
"post": {
122+
"responses": {},
123+
"x-amazon-apigateway-integration": {
124+
"httpMethod": "POST",
125+
"responseTransferMode": "STREAM",
126+
"type": "aws_proxy",
127+
"uri": {
128+
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2021-11-15/functions/${MyFunction.Arn}/response-streaming-invocations"
129+
}
130+
}
131+
}
132+
}
133+
},
134+
"swagger": "2.0"
135+
}
136+
},
137+
"Type": "AWS::ApiGateway::RestApi"
138+
},
139+
"ServerlessRestApiDeployment5a2e41cc40": {
140+
"Properties": {
141+
"Description": "RestApi deployment id: 5a2e41cc403a12dba16262d557905de64a61e2d1",
142+
"RestApiId": {
143+
"Ref": "ServerlessRestApi"
144+
},
145+
"StageName": "Stage"
146+
},
147+
"Type": "AWS::ApiGateway::Deployment"
148+
},
149+
"ServerlessRestApiProdStage": {
150+
"Properties": {
151+
"DeploymentId": {
152+
"Ref": "ServerlessRestApiDeployment5a2e41cc40"
153+
},
154+
"RestApiId": {
155+
"Ref": "ServerlessRestApi"
156+
},
157+
"StageName": "Prod"
158+
},
159+
"Type": "AWS::ApiGateway::Stage"
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)