Skip to content

execute-api: a path template never matches, a stage is never checked, and a failed Lambda answers 200 #1214

Description

@scttfrdmn

What

The execute-api plugin resolves a request to an integration by exact string equality, never checks
the stage, relays a failed Lambda as a 200, and refuses with internal English prose instead of an API
Gateway gateway response.

1. Path templates are unreachable

apigateway_plugin.go:1710 and :1679 match by equality:

if res.Path != resourcePath { continue }   // :1710path == resourcePath                     // :1679

So /{proxy+}, /{id} and every other path-parameter template is unreachable — only a literal path
matches. pathParameters in the event payload is consequently hard-coded:

"pathParameters": nil,   // :1789

A proxy resource is the single most common API Gateway shape, and a consumer's {proxy+} route
404s against substrate while working against AWS.

2. The stage is never validated

:1580-1590 extracts the stage from the URL and resolveLambdaARN (:1642-1736) reads neither the
stage nor any deployment. So /prod/users and /nonexistent-stage/users behave identically, and a
stage that was never created — or an API that was never deployed — serves traffic. What AWS answers
for an unknown stage is UNVERIFIED
: the survey did not locate the page that publishes it, so the
remedy's landing must be read before it is written rather than guessed (#671).

3. A failed Lambda is relayed as 200

:1637 passes only invokeResp.Body to the caller:

return &AWSResponse{StatusCode: 200, Body: invokeResp.Body}, nil

It discards X-Amz-Function-Error, which lambda_plugin.go:598-610 sets on a handler error and
which lambda_control.go:105-127 can seed deliberately. And parseProxyResponse (:1853-1866)
promotes a statusCode of 0 to 200, so a malformed integration response — no statusCode member at
all — also becomes a success.

AWS answers 502 for a malformed integration response and 502 for a function error, with
{"message": "Internal server error"}. So the two cases a consumer most needs to test — my handler
threw, and my handler returned the wrong shape — both report success. The seeded-failure path is worse
than unreachable: it is available (via lambda_control.go), deliberately installed by the consumer,
and then thrown away one frame later.

4. Refusals are internal prose, not gateway responses

:1890-1897 answers {"message": "<internal English sentence>"}. API Gateway publishes a fixed set
of gateway response types with defined statuses and bodies:

Type Status Body
MISSING_AUTHENTICATION_TOKEN 403 {"message":"Missing Authentication Token"}
RESOURCE_NOT_FOUND 404 {"message":"Not Found"}
INTEGRATION_FAILURE 504 {"message":"Internal server error"}
INTEGRATION_TIMEOUT 504 {"message":"Endpoint request timed out"}
API_CONFIGURATION_ERROR 500 {"message":"Internal server error"}

MISSING_AUTHENTICATION_TOKEN/403 is the one a consumer meets constantly in real use — it is what AWS
answers for an unmatched path — and a consumer who has learned to recognise it gets a substrate
sentence instead.

5. The routable name

For the record, since it is easy to get wrong when working here: the plugin's routable name is
execute-api (:1543). The string "apigateway-proxy" at plugins.go:205 interpolates only into an
init error message and is not a route.

Why this matters

These four are the difference between "substrate can host an API Gateway integration" and "substrate
can host the one integration shape that has no path parameters, on any stage name, and reports success
whatever the handler does." The proxy-resource gap alone rules out most real APIs.

The 200-on-failure defect is the most consequential thing found in this batch, because it converts a
seeded failure into a pass. CLAUDE.md's stated purpose for seeding is "to exercise the rare/slow/
failure paths a consumer's retry/poll/wait loops exist to handle"
— and here a consumer seeds exactly
such a failure through a documented control-plane endpoint, and the proxy answers 200. Their error
handling is never entered, their test goes green, and the seed's only observable effect is nothing.

Acceptance criteria

  • Resource paths match by template, so {proxy+} and {id} resolve, with the longest-specific
    match winning as AWS documents, and pathParameters carries the captured values.
  • The stage is resolved and an unknown stage is refused. Read the page that publishes the
    landing first
    — it is unverified — and record the citation in the doc comment; per ec2: an invalid block device mapping is accepted instead of refused with InvalidBlockDeviceMapping #671, do not
    borrow a code from a sibling operation.
  • A Lambda that reports a function error answers 502 with {"message": "Internal server error"}, and X-Amz-Function-Error is read rather than discarded.
  • A malformed integration response answers 502. A statusCode of 0 must not become 200.
  • Refusals are gateway responses with the published type, status and body — at minimum
    MISSING_AUTHENTICATION_TOKEN/403 for an unmatched path and RESOURCE_NOT_FOUND/404.
  • A test seeds a Lambda failure through lambda_control.go and asserts the proxy answers 502.
    This is the criterion that matters most; it must fail against today's tree, which answers 200.
  • A test drives a {proxy+} route and asserts pathParameters.
  • docs/services.md's execute-api section states how a path resolves, what an unknown stage
    answers, and what a failed integration answers.

Provenance

The gateway response types, their statuses and their default bodies are from API Gateway's
supported-gateway-response-types.html; the 502-for-malformed-integration-response and
502-for-function-error behaviours are from api-gateway-simple-proxy-for-lambda-output-format.html
and the HTTP-status troubleshooting page; longest-specific-match path resolution is from
api-gateway-method-settings-method-request.html. The landing for an unknown stage is explicitly
unverified
and must be read before that criterion is implemented.

In-tree: emulator/apigateway_plugin.go:1543, :1580-1590, :1630, :1637, :1642-1736, :1679,
:1710, :1789, :1853-1866, :1890-1897; emulator/lambda_plugin.go:598-610;
emulator/lambda_control.go:105-127; emulator/plugins.go:205. Line citations are from the tree at
the commit this issue was filed against.

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

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions