HTTP 协议具有丰富的头部,但在使用无服务器应用模型(Serverless Application Model,简称 SAM)时,除了常见的 API Gateway 和 Lambda 之外,如果要支持其他任何头部的话,则需要一些额外的努力。
这里有效(也有一些不太管用的地方)。
范围头字段:例如我最喜欢的一个 HTTP 头是 Range
,它允许客户端请求资源的一部分。例如,可以让服务器只发送大文件中的一小部分,而不用发送整个文件。
我之前写过一篇关于在 S3 中使用 Range
可以大幅提高性能的文章。简单来说,这里有一个 HTTP 请求,只需获取资源的前 256 字节的新例子:
GET /resource HTTP/1.1
Host: somedomain.tld
Range: bytes=0-255
同样,我喜欢在我的API中支持Range
,以允许客户端优化他们的网络使用情况。具体实现细节将在另一篇文章中讨论;在这里,我将只讨论如何设置这些基础架构。
一个带有 API Gateway 和 Lambda Function 的典型 SAM 模板(Serverless Application Model)可能像这样简洁。
{
"AWS模板格式版本": "2010-09-09",
"转换": "AWS::Serverless-2016-10-31",
"全局设置": {
"函数": {
"内存大小": 1024,
"运行时": "dotnet6",
"超时": 10,
"跟踪": "Active"
}
},
"资源": {
"Api": {
"类型": "AWS::Serverless::Api",
"属性": {
"阶段名称": "prod"
}
},
"处理器": {
"类型": "AWS::Serverless::Function",
"属性": {
"代码URI": ".",
"处理器": "Handler::Handler.Function::FunctionHandler",
"事件": {
"根": {
"类型": "Api",
"属性": {
"REST API标识": {
"Ref": "Api"
},
"路径": "/resource",
"方法": "POST"
}
}
}
}
}
}
}
通过这种简单的设置,Lambda 函数将接收到一个代表 HTTP 请求的结构化对象。请求对象包含路径、头部信息、正文和其他元数据,实现可以利用这些信息。在这个例子中,我们只是简单地在响应中回显请求对象。
public class Function
{
public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyRequest request, ILambdaContext context)
{
// 返回一个新的APIGatewayProxyResponse对象
return new()
{
// 设置HTTP状态码为200
StatusCode = (int)HttpStatusCode.OK,
// 设置Body是否以Base64编码为false
IsBase64Encoded = false,
// 将请求序列化为字符串
Body = JsonSerializer.Serialize(request)
};
}
}
使用 curl
,我们现在可以测试一下发送给 Lambda 的请求。
使用 `curl` 命令请求 `https://{api_id}.execute-api.{region}.amazonaws.com/prod/resource`,请将 `{api_id}`、`{region}` 和 `resource` 替换成实际的值。
响应文本展示了Lambda函数接收到的JSON格式的内容(已省略和删除了一些内容)。
{
"Resource": "/resource",
"Path": "/resource",
"HttpMethod": "GET",
"Headers": {
"Accept": "*/*",
"CloudFront-Forwarded-Proto": "CloudFront 转发协议",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-ASN": "7xx2",
"CloudFront-Viewer-Country": "US",
"Host": "abc123.execute-api.us-north-3.amazonaws.com",
"User-Agent": "用户代理",
"Via": "经由 2.0 abc123.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "X-Amz-Cf-ID",
"X-Amzn-Trace-Id": "X-Amzn-跟踪ID",
"X-Forwarded-For": "X-Forwarded-For (转发的IP地址)",
"X-Forwarded-Proto": "X-Forwarded-协议"
},
...
}
我们可以看到curl默认发送的User-Agent
和Accept
头部信息确实被Lambda接收到了。这很棒——这些头部信息非常重要。现在,让我们看看在curl命令里加上Range
会发生什么事:
使用curl命令从API获取资源的前256个字节,并显示详细信息。
curl "https://{api_id}.execute-api.{region}.amazonaws.com/prod/resource" \
-H "Range: bytes=0-255" \
--verbose
我们得到了和之前一模一样的回复。换句话说,Lambda 没有接收到 Range
头。
为了搞清楚发生了什么事,我们可以在 curl
命令后面加上 --verbose
选项来查看它实际发送了什么,并确认 Range
存在:
```curl --verbose
...
> GET (/获取资源) /prod/resource HTTP/2
> Host: jq2dpl12da.execute-api.us-west-2.amazonaws.com
> user-agent (用户代理): curl/7.68.0
> accept (接受类型): */*
> range (范围): bytes=0-255
...
好了,所以`curl`做了它该做的事,但在AWS那边却不知道为什么没接收到信息。那问题出在哪里呢?Lambda为什么没有接收到这个请求?
好吧,说白了就是,这不是 AWS 问题,但它确实挺烦的。
# 瑞典为什么不工作改为为什么这不管用
# 瑞典为什么这不管用
# 为什么这不管用
默认情况下,API网关只传递少量标头给后端集成(例如这里的 Lambda 函数)。除非特别配置,否则其他所有标头都将被忽略。
这是一个安全特性,虽然这样可能是最好的,但支持的头部如此之少,让人感到非常头疼,我们需要费很大劲才能让它正常运行。
嗯,那咱们再试一次吧。
# 又一次不成功的开端
阅读 [SAM 文档](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html#sam-function-api-requestparameters),似乎只需将该标头作为请求参数添加到函数的 `Events` 节点中即可解决问题。我们来试试这个——这是模板更新后的部分:
```yaml
Events:
我的API事件:
类型: API
属性:
REST API ID: !Ref MyApi
路径: /my/path
方法: get
请求参数:
header-key-name: "true"
...
"Events": {
"Root": {
"Type": "Api",
"Properties": {
"RestApiId": {
"Ref": "Api"
},
"Path": "/resource",
"Method": "GET",
"RequestParameters": [
"method.request.header.Range"
]
}
}
}
...
再次测试(再次使用相同的 curl 命令)也没有成功。这真是让人沮丧极了——遇到了两个障碍(到目前为止),我们依然无法访问 Lambda 的请求头。这是怎么回事?
一个不太显而易见的答案是,SAM 实现的请求参数仅仅创建了“method”部分,并没有创建将实际值映射到 Lambda 函数负载的“integration”部分。这一点在 UI 配置中一目了然。
因为 SAM 没有设置好需要的配置,所以集成请求就没有办法把那个 Range 头传给 Lambda 函数。
也许我们可以不用 Events
属性来连接 Lambda 和 API,而是可以使用 API 的 DefinitionBody
属性。我们需要在 OpenAPI 中定义端点,并使用 AWS 扩展来连接 Lambda。经过这些更改,模板最终看起来如下所示:
...
"Api": {
"Type": "AWS::Serverless::Api",
"Properties": {
"StageName": "prod",
"MergeDefinitions": true,
"DefinitionBody": {
"openapi": "3.0.1",
"info": {
"title": "my-api",
"version": "1.0"
},
"paths": {
"/resource": {
"get": {
"parameters": [
{
"name": "Range",
"in": "header",
"schema": {
"type": "string"
}
}
],
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Handler.Arn}/invocations"
},
"requestParameters": {
"integration.request.header.Range": "method.request.header.Range"
}
}
}
}
}
}
}
},
"Handler": {
"Type": "AWS::Serverless::Function",
"Properties": {
"CodeUri": "../src/Handler",
"Handler": "Handler::Handler.Function::FunctionHandler"
}
}
...
而且……还是没有运气。现在我们用curl请求时,收到一个500
错误。
错误信息是有相关谷歌搜索结果的,解释了这个问题:当使用Events
时,SAM 会自动为 Lambda 函数添加一个资源权限,允许 API Gateway 调用该函数,但在使用 OpenAPI 扩展时则不会这样。为什么?我不知道。
添加一个这样的 AWS::Lambda::Permission
资源帮助我们解决了最后的问题,模板如下所示:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Globals": {
"Function": {
"MemorySize": 1024,
"Runtime": "dotnet6",
"Timeout": 10,
"Tracing": "Active"
}
},
"Resources": {
"Api": {
"Type": "AWS::Serverless::Api",
"Properties": {
"StageName": "prod",
"MergeDefinitions": true,
"DefinitionBody": {
"openapi": "3.0.1",
"info": {
"title": "my-api",
"version": "1.0"
},
"paths": {
"/resource": {
"get": {
"parameters": [
{
"name": "Range",
"in": "header",
"schema": {
"type": "string"
}
}
],
"x-amazon-apigateway-integration": {
"httpMethod": "POST",
"type": "aws_proxy",
"uri": {
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Handler.Arn}/invocations"
},
"requestParameters": {
"integration.request.header.Range": "method.request.header.Range"
}
}
}
}
}
}
}
},
"Handler": {
"Type": "AWS::Serverless::Function",
"Properties": {
"CodeUri": "../src/Handler",
"Handler": "Handler::Handler.Function::FunctionHandler"
}
},
"HandlerPermission": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": {
"Fn::GetAtt": [
"Handler",
"Arn"
]
},
"Action": "lambda:InvokeFunction",
"Principal": "apigateway.amazonaws.com"
}
}
}
}
这已经与我最初使用的简单模板完全不同了。但是,现在使用curl命令终于可以看到Range
头已经成功传递到了Lambda(耶~)。
这样的经历是程序员挫败感的重要来源。
- 一个非常常见的 HTTP 使用场景在“即插即用”时无法直接使用
- 解决该问题且保持 SAM 模板惯用法的明显路径也无法直接使用。
- 脱离 SAM 惯用法可以解决问题,但会涉及额外的复杂性。
我很乐意记录哪些有效的方法,但希望 AWS 能提供更清晰的文档,说明如何满足这个简单且常见的需求。如果 SAM 能让这一切变得简单,就更好了。
想了解更多详情吗?看这个例子在GitHub上的示例。
共同学习,写下你的评论
评论加载中...
作者其他优质文章