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
- 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).
- Run
sam local start-api against the synthesized template.
- 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]
- 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.
SAM Local: REQUEST authorizer is dropped when a resource has both an
ANYmethod (with authorizer) and an explicitOPTIONSmethod (without one)Environment
1.151.02.200.129.6.1sam local start-api -t <template.json> --port <port> --skip-pull-image --warm-containers LAZYSummary
When a REST API resource has an
ANYmethod protected by aREQUESTauthorizer, and the same resource also has an explicitOPTIONSmethod withAuthorizationType: NONE(a common pattern for CORS preflight handled by the Lambda itself, rather than API Gateway'sMOCKCORS integration),sam local start-apiappears 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:
OPTIONSbypasses the authorizer (as configured), whileANY/other explicit methods still require it. The divergence is specific to SAM Local's routing/authorizer resolution.Minimal repro (template shape)
(Both methods point at the same Lambda; the Lambda itself branches on
event.httpMethodto answer preflight vs. real requests.)Steps to reproduce
ANYmethod with aREQUESTauthorizer, plus an explicitOPTIONSmethod withAuthorizationType: NONE, bothAWS_PROXYto the same function, on the same resource).sam local start-apiagainst the synthesized template.OPTIONSappearing twice) collapsed into one mount:GETrequest to a path under that resource, with no authorizer-satisfying credentials attached.Expected
The authorizer Lambda is invoked before the
GETrequest reachesAuthorizedFunction, 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
GETrequest is passed straight through toAuthorizedFunctionwith no authorizer context attached toevent.requestContext.authorizer, as if no authorizer were configured on the route at all.Notes
OPTIONS, OPTIONSentry 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.OPTIONSmethod (and instead lettingAWS::Serverless::Api's auto-generatedMOCKCORS 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 CORSAccess-Control-Allow-Originvalue dynamically, always returning a static default regardless of the incomingOriginheader.