Bug: REQUEST authorizer is dropped when a resource has both an `ANY` method (with authorizer) and an explicit `OPTIONS` method (without one)
- 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
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