aws / aws/aws-sam-cli

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

Open
#9,165 1 comment 0 reactions 0 assignees View on GitHub
stage/needs-triage
Dominant language
Python
Stars
6.7k
Forks
1.2k
Avg merge
1d 10h
Merged PRs (30d)
52

Description

# 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 --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)

```yaml
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 at http://127.0.0.1:/{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.

Contributor guide

Open the contributing guide

Research direction

Start with the `sam local start-api` entry point and reproduce the route-mounting output using the template shape in the report. Trace how an `ANY` method and an explicit `OPTIONS` method are merged and how the REQUEST authorizer is resolved; done means GET invokes the authorizer while OPTIONS remains unauthenticated, matching the expected behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
aws, docker, python
Domain
api, authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.