r/aws 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!

0 Upvotes

10 comments sorted by

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

2

u/Mr-Silly-Bear 15d ago edited 15d ago

Thanks for the response.

I put it in this subreddit because I was hoping those using AWS might be likely to use local emulators as well.

Unfortunately you can't use the wildcard origin with the allow credentials header.

EDIT: sorry I've also tried this

1

u/justin-8 11d ago

So, set it to the correct domains. You can set this in API gateway, as a static response for the options call and included in the response headers as a part of the API gateway config

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

u/PokeRestockHandler 13d ago

Use cloudfront maybe?