Skip to content

MergeDefinitions: true merges Method: ANY API events as any instead of x-amazon-apigateway-any-method #4000

Description

@tiagohconte

Description

When an AWS::Serverless::Api has an inline DefinitionBody and MergeDefinitions: true, a function Api event (under AWS::Serverless::Function) with Method: ANY is merged into the body under the lowercase key any. Without MergeDefinitions (SAM-generated body), the same event correctly produces x-amazon-apigateway-any-method.

any is neither a valid OpenAPI/Swagger operation nor the API Gateway extension, so API Gateway silently ignores it. The deployment succeeds, but no ANY method is created on the resource (only OPTIONS if Cors is set). Every request returns 403 from API Gateway and never reaches the Lambda:

Invalid key=value pair (missing equal-sign) in Authorization header ...

A second, related defect: with MergeDefinitions: true and paths empty ({}), null or missing in DefinitionBody, the event's path is left out of the output entirely.

Steps to reproduce

Transform: AWS::Serverless-2016-10-31
Resources:
  MyApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: prod
      MergeDefinitions: true
      DefinitionBody:
        openapi: "3.0.1"
        info:
          title: repro
          version: "1.0"
        paths:
          /other:
            x-amazon-apigateway-any-method:
              x-amazon-apigateway-integration:
                type: http_proxy
                httpMethod: ANY
                uri: https://example.com
  MyFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.11
      Handler: index.handler
      InlineCode: "def handler(e, c): return {}"
      Events:
        AnyApi:
          Type: Api
          Properties:
            Path: /items/{proxy+}
            Method: ANY
            RestApiId: !Ref MyApi
  1. Transform the template (I used SAM translator).
  2. Inspect Resources.MyApi.Properties.Body.paths["/items/{proxy+}"].

Observed result

"/items/{proxy+}": {
  "any": {
    "x-amazon-apigateway-integration": {
      "type": "aws_proxy",
      "httpMethod": "POST",
      "uri": { "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" }
    },
    "responses": {}
  }
}
Configuration Method keys on /items/{proxy+}
No DefinitionBody (SAM-generated), Method: ANY x-amazon-apigateway-any-method
MergeDefinitions: true, OpenAPI 3.0.1, Method: ANY any
MergeDefinitions: true, Swagger 2.0, Method: ANY any
MergeDefinitions: true + Cors, Method: ANY any, options
MergeDefinitions: true, paths: {}, Method: ANY path missing
MergeDefinitions: true, Method: GET get (correct)

Expected result

With MergeDefinitions: true, an Api event with Method: ANY is merged as x-amazon-apigateway-any-method, the same as when SAM generates the body, and the event path is always present in the merged body, including when the inline paths is empty, null or missing.

Possible cause

Api.to_cloudformation lowercases the event method (self.Method = self.Method.lower()). In Api._get_merged_definitions (samtranslator/model/eventsources/push.py), the normalized name is computed but only used to read SAM's generated body. The lookup of the inline method and the write-back both use the raw self.Method:

merged_definition_body = source_definition_body.copy()
source_body_paths = merged_definition_body.get("paths") or {}   # (2) new dict never assigned back

try:
    path_method_body = dict_deep_get(source_body_paths, [self.Path, self.Method]) or {}   # (1) looks up "any"
...
method = editor._normalize_method_name(self.Method)   # "x-amazon-apigateway-any-method"
generated_path_method_body = dest_definition_body["paths"][self.Path][method]
...
if self.Path not in source_body_paths:
    source_body_paths[self.Path] = {self.Method: merged_path_method_body}   # (1) writes "any"
source_body_paths[self.Path][self.Method] = merged_path_method_body        # (1) writes "any"
  1. Wrong key: self.Method is any, so the inline x-amazon-apigateway-any-method entry is never found (explaining the duplicate keys) and the merged result is written under any.
  2. Dropped path: when paths is falsy, or {} creates a new dict that is never stored back on merged_definition_body, so the merged path is discarded.

The existing snapshot tests api_merge_definitions_with_any_method and api_with_merge_definitions_null_paths currently lock in the second defect: their expected output contains no event path.

Proposed solution

Use the normalized method name for lookup, validation message and write-back, and always store the paths dict on the merged body:

         merged_definition_body = source_definition_body.copy()
         source_body_paths = merged_definition_body.get("paths") or {}
+        merged_definition_body["paths"] = source_body_paths
+
+        # Normalized version of HTTP Method. It also handle API Gateway specific methods like "ANY"
+        method = editor._normalize_method_name(self.Method)

         try:
-            path_method_body = dict_deep_get(source_body_paths, [self.Path, self.Method]) or {}
+            path_method_body = dict_deep_get(source_body_paths, [self.Path, method]) or {}
         except InvalidValueType as e:
             raise InvalidResourceException(api_id, f"Property 'DefinitionBody' is invalid: {e!s}") from e

-        sam_expect(path_method_body, api_id, f"DefinitionBody.paths.{self.Path}.{self.Method}").to_be_a_map()
+        sam_expect(path_method_body, api_id, f"DefinitionBody.paths.{self.Path}.{method}").to_be_a_map()

-        # Normalized version of HTTP Method. It also handle API Gateway specific methods like "ANY"
-        method = editor._normalize_method_name(self.Method)
         dest_definition_body = editor.swagger
         generated_path_method_body = dest_definition_body["paths"][self.Path][method]
         # this guarantees that the merged definition use SAM generated value for a conflicting key
         merged_path_method_body = {**path_method_body, **generated_path_method_body}

         if self.Path not in source_body_paths:
-            source_body_paths[self.Path] = {self.Method: merged_path_method_body}
-        source_body_paths[self.Path][self.Method] = merged_path_method_body
+            source_body_paths[self.Path] = {}
+        source_body_paths[self.Path][method] = merged_path_method_body

Test changes:

  • Regenerate api_merge_definitions_with_any_method and api_with_merge_definitions_null_paths outputs (all partitions). They will now contain the /proxy path under x-amazon-apigateway-any-method, with new deployment logical IDs.
  • Add a transform test (e.g. api_merge_definitions_with_existing_any_method) where the inline OpenAPI 3.0.1 body already declares x-amazon-apigateway-any-method on the event path plus another path, asserting a single merged x-amazon-apigateway-any-method key that keeps inline fields and uses SAM's integration.

Behavior note: an inline method declared under the (invalid) lowercase key any is no longer matched for merging. Since API Gateway never honored that key, this should not affect working templates.

With the change applied, the full unit test suite passes and the reproduction template yields x-amazon-apigateway-any-method (plus options with Cors) for both the populated and empty paths cases.

Additional environment details

  1. OS: macOS (transform reproduced locally); deployed via AWS CloudFormation
  2. sam --version: SAM CLI, version 1.166.1 (aws-sam-translator 1.110.0); also reproduced on develop (aws-sam-translator 1.113.0)
  3. AWS region: eu-west-1

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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