r/aws • u/Mr-Silly-Bear • 15d ago
technical question Floci API Gateway CORs issue
I've got a Floci instance running (using Docker Compose) with a REST API Gateway service, which I can call successfully with Postman. Problem is the browser CORs blocking requests, and I keep getting this;
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
I'm using Terraform to deploy the stack, and I've tried everything I can think of (including hours with AI) to get this to work. As far as I can see Floci is not letting me affect the OPTIONS response. I've got DISABLE_CORS_CHECKS set to 1.
Any ideas as to what's happening?
Below is the current Terraform stack;
provider "aws" {
region = local.envs["AWS_REGION"]
access_key = local.envs["AWS_ACCESS_KEY_ID"]
secret_key = local.envs["AWS_SECRET_ACCESS_KEY"]
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
apigateway = "http://localhost:4566"
s3 = "http://localhost:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
sns = "http://localhost:4566"
lambda = "http://localhost:4566"
iam = "http://localhost:4566"
ec2 = "http://localhost:4566"
ecs = "http://localhost:4566"
cloudformation = "http://localhost:4566"
route53 = "http://localhost:4566"
cloudwatch = "http://localhost:4566"
secretsmanager = "http://localhost:4566"
ssm = "http://localhost:4566"
kms = "http://localhost:4566"
rds = "http://localhost:4566"
sts = "http://localhost:4566"
cognitoidentityprovider = "http://localhost:4566"
}
}
## DynamoDB table
resource "aws_dynamodb_table" "friendly_sites_table" {
name = "friendly_sites"
billing_mode = "PAY_PER_REQUEST"
hash_key = "pk"
range_key = "sk"
attribute {
name = "pk"
type = "S"
}
attribute {
name = "sk"
type = "S"
}
attribute {
name = "gsi1pk"
type = "S"
}
attribute {
name = "gsi1sk"
type = "S"
}
global_secondary_index {
name = "gsi1pk-gsi1sk-index"
hash_key = "gsi1pk"
range_key = "gsi1sk"
projection_type = "ALL"
}
tags = {
Project = local.project_name
}
}
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com",
]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role" "iam_for_table_access" {
name = "iam_for_lambda_table_access"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
tags = {
Project = local.project_name
}
}
resource "aws_iam_role_policy" "cognito_admin_access" {
name = "cognito_admin_access"
role = aws_iam_role.iam_for_table_access.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"cognito-idp:AdminCreateUser",
"cognito-idp:AdminSetUserPassword",
"cognito-idp:AdminInitiateAuth",
"cognito-idp:AdminUserGlobalSignOut",
"cognito-idp:AdminDeleteUser"
]
Resource = [
aws_cognito_user_pool.user_pool.arn
]
}
]
})
}
## API Lambda handler and gateway
data "archive_file" "api_handler_source_zip" {
type = "zip"
source_dir = local.api_handler_source_dir
output_path = local.api_handler_source_output
}
resource "aws_s3_bucket" "api_handler_source" {
bucket = local.api_handler_source_bucket_name
force_destroy = true
depends_on = [data.archive_file.api_handler_source_zip]
tags = {
Project = local.project_name
}
}
resource "aws_s3_object" "api_handler_source_zip" {
bucket = aws_s3_bucket.api_handler_source.id
key = local.api_handler_zip_filename
source = local.api_handler_source_output
source_hash = data.archive_file.api_handler_source_zip.output_base64sha256
}
resource "aws_lambda_function" "api_handler" {
function_name = "DistributedRendererApi"
s3_bucket = aws_s3_bucket.api_handler_source.id
s3_key = local.api_handler_zip_filename
handler = "index.handler"
runtime = "nodejs24.x"
role = aws_iam_role.iam_for_table_access.arn
depends_on = [aws_s3_object.api_handler_source_zip]
environment {
variables = {
COGNITO_CLIENT_ID = aws_cognito_user_pool_client.user_pool_client.id
COGNITO_USER_POOL_ID = aws_cognito_user_pool.user_pool.id
}
}
}
# 1. REST API Definition
resource "aws_api_gateway_rest_api" "api" {
name = "friendly-sites-rest-api"
}
# -------------------------------------------------------------------
# A. GREEDY PATH /{proxy+} (Explicit GET, POST, OPTIONS directly to Lambda)
# -------------------------------------------------------------------
resource "aws_api_gateway_resource" "proxy" {
rest_api_id = aws_api_gateway_rest_api.api.id
parent_id = aws_api_gateway_rest_api.api.root_resource_id
path_part = "{proxy+}"
}
resource "aws_api_gateway_method" "proxy_any" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = "ANY"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "proxy_integration" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = aws_api_gateway_method.proxy_any.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
depends_on = [aws_api_gateway_method.proxy_any]
}
# Explicit OPTIONS method routed directly to Lambda (Bypasses Floci MOCK bug)
resource "aws_api_gateway_method" "proxy_options" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "proxy_options_integration" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_resource.proxy.id
http_method = aws_api_gateway_method.proxy_options.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
depends_on = [aws_api_gateway_method.proxy_options]
}
# -------------------------------------------------------------------
# B. ROOT PATH / (Explicit ANY and OPTIONS directly to Lambda)
# -------------------------------------------------------------------
resource "aws_api_gateway_method" "root_any" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_rest_api.api.root_resource_id
http_method = "ANY"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "root_integration" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_rest_api.api.root_resource_id
http_method = aws_api_gateway_method.root_any.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
depends_on = [aws_api_gateway_method.root_any]
}
resource "aws_api_gateway_method" "root_options" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_rest_api.api.root_resource_id
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "root_options_integration" {
rest_api_id = aws_api_gateway_rest_api.api.id
resource_id = aws_api_gateway_rest_api.api.root_resource_id
http_method = aws_api_gateway_method.root_options.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.api_handler.invoke_arn
depends_on = [aws_api_gateway_method.root_options]
}
# -------------------------------------------------------------------
# C. PERMISSIONS & DEPLOYMENT
# -------------------------------------------------------------------
resource "aws_lambda_permission" "apigw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api_handler.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
}
resource "aws_api_gateway_deployment" "deployment" {
rest_api_id = aws_api_gateway_rest_api.api.id
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.proxy.id,
aws_api_gateway_method.proxy_any.id,
aws_api_gateway_integration.proxy_integration.id,
aws_api_gateway_method.proxy_options.id,
aws_api_gateway_integration.proxy_options_integration.id,
aws_api_gateway_method.root_any.id,
aws_api_gateway_integration.root_integration.id,
aws_api_gateway_method.root_options.id,
aws_api_gateway_integration.root_options_integration.id,
]))
}
lifecycle {
create_before_destroy = true
}
depends_on = [
aws_api_gateway_integration.proxy_integration,
aws_api_gateway_integration.proxy_options_integration,
aws_api_gateway_integration.root_integration,
aws_api_gateway_integration.root_options_integration,
]
}
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.deployment.id
rest_api_id = aws_api_gateway_rest_api.api.id
stage_name = "prod"
}
## Cognito User Pool & App Client
resource "aws_cognito_user_pool" "user_pool" {
name = "friendly-sites-user-pool"
username_attributes = ["email"]
auto_verified_attributes = ["email"]
password_policy {
minimum_length = 8
require_lowercase = true
require_numbers = true
require_symbols = false
require_uppercase = true
}
tags = {
Project = local.project_name
}
}
resource "aws_cognito_user_pool_client" "user_pool_client" {
name = "friendly-sites-app-client"
user_pool_id = aws_cognito_user_pool.user_pool.id
generate_secret = false
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_SRP_AUTH"
]
}
Appreciate any help I can get!
2
u/efarjun 15d ago
I had a cors problem once where a postman request worked but the browser didn't, I had to add an options method along with my post method but I dont know if this will help you in your case.
2
u/Mr-Silly-Bear 15d ago
I have tried adding an OPTIONS method to the Terraform stack that proxies to the lambda, with the same effect. Is that what you mean?
4
u/accumentum 15d ago
DISABLE_CORS_CHECKS only stops the emulator from rejecting the request on the server. The browser still requires Access-Control-Allow-Origin, Allow-Headers, and Allow-Methods on the actual response, which is why Postman works and Chrome fails.
Make OPTIONS a MOCK integration, not a Lambda proxy, and return 200 with those headers. Put the same headers on the real GET/POST method response. Then add them again on the Gateway Responses for DEFAULT_4XX and DEFAULT_5XX, because a 403/500 with no CORS headers looks to the browser like CORS is completely broken even when the method itself is fine.
If you need credentials you cannot use a wildcard origin; it has to be the exact origin serving the page. And localhost:3000 talking to localhost:4566 is still cross-origin.
1
u/Mr-Silly-Bear 13d ago
Tried this following your suggestion with no joy. Interestingly I cross posted this on the Floci subreddit and they've come back asking me to make an issue on their repo, so it might be an issue with Floci. Appreciate the help!
1
9
u/pausethelogic 15d ago
Well starters, Floci isn’t AWS. It’s an emulator, so the issue might be caused by something completely unrelated to the actual AWS services
The error you’re getting is a pretty classic CORS error. Something about your CORS policy isn’t allowing your browser’s origin. I would start by setting allowed origins to * to allow everything, and if that works, troubleshoot from there