为了账号安全,请及时绑定邮箱和手机立即绑定

Lambda函数通过API Gateway接收HTTP请求头(如Range头)的方法

标签:
云计算 AWS API

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-AgentAccept头部信息确实被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(耶~)。

摘要

这样的经历是程序员挫败感的重要来源。

  1. 一个非常常见的 HTTP 使用场景在“即插即用”时无法直接使用
  2. 解决该问题且保持 SAM 模板惯用法的明显路径也无法直接使用。
  3. 脱离 SAM 惯用法可以解决问题,但会涉及额外的复杂性。

我很乐意记录哪些有效的方法,但希望 AWS 能提供更清晰的文档,说明如何满足这个简单且常见的需求。如果 SAM 能让这一切变得简单,就更好了。

想了解更多详情吗?看这个例子在GitHub上的示例

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消