Terraform配置AWS API Gateway OPTIONS预检请求失败排查
我通过Terraform创建了带Lambda Proxy集成的AWS API Gateway REST API,Lambda函数负责对DynamoDB表执行CRUD操作。除GET和OPTIONS请求外,其余请求都配置了Cognito用户池授权器。
我用Terraform模块为所有资源配置了OPTIONS预检请求的MOCK集成,设置了Access-Control-Allow-Headers、Access-Control-Allow-Methods、Access-Control-Allow-Origin:
Terraform CORS模块代码
api/cors/main.tf
# api/cors/main.tf resource "aws_api_gateway_method" "cors_method" { rest_api_id = var.api_id resource_id = var.resource_id http_method = "OPTIONS" authorization = "NONE" } resource "aws_api_gateway_method_response" "cors_method_response" { rest_api_id = var.api_id resource_id = var.resource_id http_method = aws_api_gateway_method.cors_method.http_method status_code = "200" response_models = { "application/json" = "Empty" } response_parameters = { "method.response.header.Access-Control-Allow-Headers" = true "method.response.header.Access-Control-Allow-Methods" = true, "method.response.header.Access-Control-Allow-Origin" = true, } depends_on = [aws_api_gateway_method.cors_method] } resource "aws_api_gateway_integration" "cors_integration" { rest_api_id = var.api_id resource_id = var.resource_id http_method = aws_api_gateway_method.cors_method.http_method type = "MOCK" depends_on = [aws_api_gateway_method.cors_method] } resource "aws_api_gateway_integration_response" "cors_integration_response" { rest_api_id = var.api_id resource_id = var.resource_id http_method = aws_api_gateway_method.cors_method.http_method status_code = aws_api_gateway_method_response.cors_method_response.status_code response_parameters = { "method.response.header.Access-Control-Allow-Headers" = "'${join(",", var.headers)}'" "method.response.header.Access-Control-Allow-Methods" = "'${join(",", var.methods)}'", "method.response.header.Access-Control-Allow-Origin" = "'${join(",", var.origins)}'", } depends_on = [aws_api_gateway_method_response.cors_method_response] }
api/cors/variables.tf
# api/cors/variables.tf variable "api_id" {} variable "resource_id" {} variable "origins" { type = list(string) default = ["http://localhost:3000"] } variable "methods" { type = list(string) } variable "headers" { type = list(string) default = ["Content-Type", "X-Amz-Date", "Authorization", "X-Api-Key", "X-Amz-Security-Token"] }
api/main.tf(相关片段)
# api/main.tf # API, other API resources, deployment, stage also defined here # /users/{username}/follow resource "aws_api_gateway_resource" "follow" { rest_api_id = aws_api_gateway_rest_api.api.id parent_id = aws_api_gateway_resource.username.id path_part = "follow" } module "FollowCORS" { source = "./cors" api_id = aws_api_gateway_rest_api.api.id resource_id = aws_api_gateway_resource.follow.id methods = ["DELETE", "OPTIONS", "PUT"] }
Lambda函数代码(对应端点)
// Lambda for this endpoint/method const AWS = require('aws-sdk'); // Set up DynamoDB DocumentClient exports.handler = async (event) => { let body = {}; let statusCode = 200; const headers = { 'Access-Control-Allow-Headers': 'Accept,Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token', 'Access-Control-Allow-Methods': 'DELETE,OPTIONS,PUT', 'Access-Control-Allow-Origin': '*', // temporary update as per suggestion 'Content-Type': 'application/json', Vary: 'Origin', }; // original `Access-Control-Allow-Origin` setting // const allowedOrigins = ['http://localhost:3000']; // const origin = event.headers.origin || ''; // if (allowedOrigins.includes(origin)) { // headers['Access-Control-Allow-Origin'] = origin; // } console.log('Event:\n', event); // Check/Get `cognito:username` from event try { // DELETE operation on DynamoDB table body.isFollowing = false; } catch (err) { console.error('Error:\n', err); statusCode = 500; body = { error: { message: err.message } }; } return { statusCode, headers, body: JSON.stringify(body) }; };
前端请求代码
Axios DELETE请求
axios .delete(`${API_BASE_URL}/users/testuser/follow`, { headers: { Authorization: `Bearer ${id_token}`, }, }) .then((response) => { console.log(response); }) .catch((error) => { console.log(error); });
Fetch DELETE请求
fetch(`${API_BASE_URL}/users/testuser/follow`, { method: 'DELETE', headers: { Authorization: `Bearer ${id_token}`, }, }) .then((res) => res.json()) .then((data) => { console.log(data); });
问题现象
- Postman可成功访问所有端点,Next.js应用中GET请求(useSWR、fetch、axios)均正常,但DELETE/PATCH/POST/PUT请求无法发起
- 收到CORS错误:
Access to fetch at 'https://{API_BASE_URL}/users/testuser/follow' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
- Fetch请求额外报错:
TypeError: Failed to fetch
- 请求未到达API,无CloudWatch日志;Network面板中axios请求仅含
Referer: http://localhost:3000/,无Origin头,也无响应头 - Postman测试OPTIONS方法时收到错误:
... not a valid key=value pair (missing equal-sign) in Authorization header ...
环境:Terraform v1.2.7(windows_amd64),hashicorp/aws版本~>4.26
1. 修复OPTIONS请求的授权配置
Postman测试OPTIONS出现的Authorization错误,说明OPTIONS方法被错误应用了Cognito授权器。虽然Terraform中显式设置了authorization = "NONE",但可能存在父资源/API级别默认授权器继承,或部署未更新的问题:
- 登录API Gateway控制台,确认OPTIONS方法的授权类型确实为
NONE - 在Terraform的
aws_api_gateway_deployment资源中添加依赖,确保配置变更后重新部署,比如:
resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = aws_api_gateway_rest_api.api.id stage_name = var.stage_name depends_on = [ # 包含所有CORS模块引用 module.FollowCORS # 其他API方法、集成资源 ] }
2. 修正CORS响应头格式
CORS规范不允许Access-Control-Allow-Origin设置多个逗号分隔的域名,只能是单个域名或*。当前MOCK集成的拼接方式不符合要求:
- 修改
api/cors/main.tf中的集成响应参数,暂时用*测试:
response_parameters = { "method.response.header.Access-Control-Allow-Headers" = "'${join(",", var.headers)}'" "method.response.header.Access-Control-Allow-Methods" = "'${join(",", var.methods)}'", "method.response.header.Access-Control-Allow-Origin" = "'*'", }
- 若需严格限制Origin,可将OPTIONS集成改为Lambda代理,在Lambda中动态匹配请求的Origin头返回
3. 确保前端请求的Origin头正常发送
Network面板无Origin头可能是Next.js代理配置问题:
- 确认
API_BASE_URL是正确的跨域地址,而非本地代理地址 - 检查
next.config.js中的代理配置,避免转发请求时丢失Origin头
4. 验证部署生效
每次修改Terraform配置后,必须重新执行terraform apply并确认API部署已更新,否则配置不会生效。
内容的提问来源于stack exchange,提问作者Larissa

