Skip to content

Bug: REQUEST authorizer is dropped when a resource has both an ANY method (with authorizer) and an explicit OPTIONS method (without one) #9165

Description

@pi-nathan

SAM Local: REQUEST authorizer is dropped when a resource has both an ANY method (with authorizer) and an explicit OPTIONS method (without one)

Environment

  • SAM CLI: 1.151.0
  • AWS CDK: 2.200.1
  • Docker: 29.6.1
  • OS: macOS 26.5.2 (arm64)
  • Command: sam local start-api -t <template.json> --port <port> --skip-pull-image --warm-containers LAZY

Summary

When a REST API resource has an ANY method protected by a REQUEST authorizer, and the same resource also has an explicit OPTIONS method with AuthorizationType: NONE (a common pattern for CORS preflight handled by the Lambda itself, rather than API Gateway's MOCK CORS integration), sam local start-api appears to merge all HTTP methods on that resource into a single mounted route — and the authorizer is never invoked for any verb on that route, including the ones that should require it (e.g. GET).

On real, deployed API Gateway this works correctly: OPTIONS bypasses the authorizer (as configured), while ANY/other explicit methods still require it. The divergence is specific to SAM Local's routing/authorizer resolution.

Minimal repro (template shape)

Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev

  MyAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Type: REQUEST
      RestApiId: !Ref Api
      IdentitySource: method.request.header.Cookie
      AuthorizerResultTtlInSeconds: 0

  ProxyResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref Api
      ParentId: !GetAtt Api.RootResourceId
      PathPart: "{proxy+}"

  AnyMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ProxyResource
      HttpMethod: ANY
      AuthorizationType: CUSTOM
      AuthorizerId: !Ref MyAuthorizer
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizedFunction.Arn}/invocations

  OptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref Api
      ResourceId: !Ref ProxyResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${AuthorizedFunction.Arn}/invocations

(Both methods point at the same Lambda; the Lambda itself branches on event.httpMethod to answer preflight vs. real requests.)

Steps to reproduce

  1. Deploy/synthesize the above shape (an ANY method with a REQUEST authorizer, plus an explicit OPTIONS method with AuthorizationType: NONE, both AWS_PROXY to the same function, on the same resource).
  2. Run sam local start-api against the synthesized template.
  3. Observe the startup log's route-mounting line for this resource — it lists all methods (including OPTIONS appearing twice) collapsed into one mount:
    Mounting <FunctionName> at http://127.0.0.1:<port>/{proxy+} [DELETE, GET, HEAD, OPTIONS, OPTIONS, PATCH, POST, PUT]
    
  4. Send a GET request to a path under that resource, with no authorizer-satisfying credentials attached.

Expected

The authorizer Lambda is invoked before the GET request reaches AuthorizedFunction, and (lacking valid credentials) the request is rejected per the authorizer's response — matching deployed API Gateway behavior.

Actual

The authorizer Lambda is never invoked. The GET request is passed straight through to AuthorizedFunction with no authorizer context attached to event.requestContext.authorizer, as if no authorizer were configured on the route at all.

Notes

  • The duplicated OPTIONS, OPTIONS entry in the mount-log method list (see step 3) suggests the route-merging logic that produces this list has a deduplication bug, which may be related to the underlying authorizer-resolution issue.
  • Removing the explicit OPTIONS method (and instead letting AWS::Serverless::Api's auto-generated MOCK CORS integration handle preflight) avoids the problem — but that MOCK integration has its own, separate limitation locally: it doesn't evaluate the VTL template used to select the CORS Access-Control-Allow-Origin value dynamically, always returning a static default regardless of the incoming Origin header.

Metadata

Metadata

Assignees

No one assigned

    Labels

    stage/needs-triageAutomatically applied to new issues and PRs, indicating they haven't been looked at.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions