CodeGenieApp / CodeGenieApp/serverless-express

`multiValueHeaders` is an unmitigated disaster

オープン
#554 コメント 0 件 リアクション 3 件 担当者 0 名 GitHub で見る
主要言語
JavaScript
スター
5.3k
フォーク
676
PR マージ指標
30日以内にマージされた PR はありません

説明

This isn't a bug in this project *per se* but something I thought I should bring to your attention.

Setting more than one header of the same name in a Lambda response is very difficult to do reliably. Here is the current state of affairs:
- APIGateway: Does not support `multiValueHeaders` in payload format "2.0", only in "1.0"
- Elastic Load Balancer: Only supports `multiValueHeaders` if you explicitly opt-in to it with `lambda.multi_value_headers.enabled`. Furthermore **it reverses the header order** which is devastating for headers like `Set-Cookie`
- Lambda Function URL: Does not support `multiValueHeaders` at all

So without metadata on the deployment and routing of the Lambda function this response is inherently unsafe. I'm honestly bewildered by how wrong the AWS team has consistently botched this 26 year old protocol.

I found this idea on StackOverflow which is honestly a brilliant workaround. You can toggle the casing in the returned headers in order to reliable send multiple headers:
https://stackoverflow.com/questions/66284664/multiple-set-cookie-headers-ignored-by-api-gateway-in-combination-with-lambda-in#answer-66317294

I'm going to further test this solution to see if it maintains the order of the headers and will report back.

Edit: It's bad:
- APIGateway Payload 1.0: Returns only the *first* header
- APIGateway Payload 2.0: Returns only the *last* header
- Elastic Load Balancer: Returns headers of each case, order still reversed. `lambda.multi_value_headers.enabled` must be false or `headers` is ignored entirely.
- Lambda URL: Returns the last header

There is a `cookies` response property which is supported by APIGateway payloads 2.0, and Lambda URL. Unsupported by APIGateway payload 1.0 & Elastic Load Balancer.

Based on this reverse engineering I think that Lambda URL is using APIGateway on the backend with payload format 2.0, though I can't find this documented anywhere.

I put together a CloudFormation stack to demonstrate the issue in AWS's services:
`execute.sh`
```sh
#!/bin/bash
set -e

STACK_NAME=lambda-header-bug
VPC_ID=$(aws ec2 describe-vpcs | jq -r '.Vpcs[] | select(.IsDefault).VpcId')
SUBNET_IDS=$(aws ec2 describe-subnets | jq -r '[ .Subnets[] | select(.VpcId == "'"$VPC_ID"'").SubnetId ] | join(",")')
aws cloudformation deploy \
--template-file Stack.yaml \
--stack-name "$STACK_NAME" \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
VpcId="$VPC_ID" \
SubnetIds="$SUBNET_IDS"
echo

OUTPUT=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" | jq '.Stacks[0].Outputs | map({ key: .OutputKey, value: .OutputValue }) | from_entries')
API_GATEWAY_URL=$(echo "$OUTPUT" | jq -r .ApiGatewayURL)
FUNCTION_URL=$(echo "$OUTPUT" | jq -r .FunctionURL)
LOAD_BALANCER_URL=$(echo "$OUTPUT" | jq -r .LoadBalancerURL)

echo "$API_GATEWAY_URL"
curl -vv "$API_GATEWAY_URL" 2>&1 | grep -i set-cookie || true
echo

echo "$FUNCTION_URL"
curl -vv "$FUNCTION_URL" 2>&1 | grep -i set-cookie || true
echo

echo "$LOAD_BALANCER_URL"
curl -vv "$LOAD_BALANCER_URL" 2>&1 | grep -i set-cookie || true

echo
echo To delete the stack, please run: aws cloudformation delete-stack --stack-name "$STACK_NAME"
```

`Stack.yaml`
```
AWSTemplateFormatVersion: 2010-09-09
Description: Lambda reversed header issue

Parameters:
VpcId:
Type: String
SubnetIds:
Type: CommaDelimitedList

Outputs:
ApiGatewayURL:
Value: !Sub ${Api.ApiEndpoint}/${Stage}
FunctionURL:
Value: !GetAtt FunctionURL.FunctionUrl
LoadBalancerURL:
Value: !Sub http://${LoadBalancer.DNSName}

Resources:
# Function definition
Function:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
MemorySize: 512
Role: !GetAtt Role.Arn
Runtime: nodejs16.x
Timeout: 30
Code:
ZipFile:
|
exports.handler = async (event) => ({
statusCode: 200,
body: JSON.stringify(event),
isBase64Encoded: false,
cookies: [
'a=1',
'a=2',
],
multiValueHeaders: {
'x-set-cookie': [
'a=1',
'a=2'
],
},
'content-type': [ 'text/plain' ],
});

Role:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: lambda.amazonaws.com

LogGroup:
Type: AWS::Logs::LogGroup
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
LogGroupName: !Sub /aws/lambda/${Function}
RetentionInDays: 1

LogPolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: AllowCloudWatch
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !GetAtt LogGroup.Arn
Roles:
- !Ref Role

# Public function URL
FunctionURL:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
TargetFunctionArn: !GetAtt Function.Arn

PermissionURL:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunctionUrl
FunctionName: !GetAtt Function.Arn
FunctionUrlAuthType: NONE
Principal: "*"

# Load Balancer
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
SecurityGroups:
- !Ref SecurityGroup
Subnets: !Ref SubnetIds

SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Ref AWS::StackName
VpcId: !Ref VpcId
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: tcp
ToPort: 80
- CidrIpv6: ::/0
FromPort: 80
IpProtocol: tcp
ToPort: 80

Listener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: 500

# Load balancer target
PermissionInvoke:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt Function.Arn
Principal: elasticloadbalancing.amazonaws.com

TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
TargetType: lambda
Targets:
- Id: !GetAtt Function.Arn
TargetGroupAttributes:
- Key: lambda.multi_value_headers.enabled
Value: true

ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
ListenerArn: !Ref Listener
Priority: 1
Actions:
- Type: forward
ForwardConfig:
TargetGroups:
- TargetGroupArn: !Ref TargetGroup
Conditions:
- Field: http-request-method
HttpRequestMethodConfig:
Values:
- GET
- HEAD
- OPTIONS

# APIGateway
Api:
Type: AWS::ApiGatewayV2::Api
Properties:
Name: !Ref AWS::StackName
ProtocolType: HTTP

Integration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref Api
CredentialsArn: !GetAtt ApiRole.Arn
IntegrationType: AWS_PROXY
IntegrationUri: !Sub arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations
# Payload format 1.0 supports `multiValueHeaders`, but not `cookies`
# Payload format 2.0 does not support `multiValueHeaders`, but supports `cookies`
PayloadFormatVersion: "2.0"

Route:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref Api
RouteKey: $default
Target: !Sub integrations/${Integration}

Stage:
Type: AWS::ApiGatewayV2::Stage
Properties:
ApiId: !Ref Api
AutoDeploy: true
StageName: latest

ApiRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: apigateway.amazonaws.com
Policies:
- PolicyName: ApiGateway
PolicyDocument:
Version: 2012-10-17
Statement:
- Action: lambda:InvokeFunction
Effect: Allow
Resource: !Sub ${Function.Arn}
```

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

まず、提供されている execute.sh と Stack.yaml の再現手順を確認し、AWS API Gateway、Lambda URL、load balancer におけるその動作を、このプロジェクトのレスポンス処理と比較します。完了とするには、プロジェクトレベルのスコープを確認し、変更またはドキュメント化のいずれかの結果について合意する必要があります。現在の issue では、変更対象の repository ファイルを特定しないまま、外部の AWS の制限が報告されています。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
aws, javascript, nodejs, shell, yaml
領域
api, backend, cloud
issue の種類
バグ
難易度
5/5
見積もり時間
1週間以上
活発さ
停滞
明瞭さ
説明が足りない
初心者へのやさしさ
20/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。