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 } // :1710
… path == 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
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.
What
The
execute-apiplugin resolves a request to an integration by exact string equality, never checksthe 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:1710and:1679match by equality:So
/{proxy+},/{id}and every other path-parameter template is unreachable — only a literal pathmatches.
pathParametersin the event payload is consequently hard-coded:A proxy resource is the single most common API Gateway shape, and a consumer's
{proxy+}route404s against substrate while working against AWS.
2. The stage is never validated
:1580-1590extracts the stage from the URL andresolveLambdaARN(:1642-1736) reads neither thestage nor any deployment. So
/prod/usersand/nonexistent-stage/usersbehave identically, and astage 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
:1637passes onlyinvokeResp.Bodyto the caller:It discards
X-Amz-Function-Error, whichlambda_plugin.go:598-610sets on a handler error andwhich
lambda_control.go:105-127can seed deliberately. AndparseProxyResponse(:1853-1866)promotes a
statusCodeof0to 200, so a malformed integration response — nostatusCodemember atall — 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 handlerthrew, 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-1897answers{"message": "<internal English sentence>"}. API Gateway publishes a fixed setof gateway response types with defined statuses and bodies:
MISSING_AUTHENTICATION_TOKEN{"message":"Missing Authentication Token"}RESOURCE_NOT_FOUND{"message":"Not Found"}INTEGRATION_FAILURE{"message":"Internal server error"}INTEGRATION_TIMEOUT{"message":"Endpoint request timed out"}API_CONFIGURATION_ERROR{"message":"Internal server error"}MISSING_AUTHENTICATION_TOKEN/403 is the one a consumer meets constantly in real use — it is what AWSanswers 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"atplugins.go:205interpolates only into aninit 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
{proxy+}and{id}resolve, with the longest-specificmatch winning as AWS documents, and
pathParameterscarries the captured values.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.
{"message": "Internal server error"}, andX-Amz-Function-Erroris read rather than discarded.statusCodeof0must not become 200.MISSING_AUTHENTICATION_TOKEN/403 for an unmatched path andRESOURCE_NOT_FOUND/404.lambda_control.goand asserts the proxy answers 502.This is the criterion that matters most; it must fail against today's tree, which answers 200.
{proxy+}route and assertspathParameters.docs/services.md'sexecute-apisection states how a path resolves, what an unknown stageanswers, 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 and502-for-function-error behaviours are from
api-gateway-simple-proxy-for-lambda-output-format.htmland 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 explicitlyunverified 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 atthe commit this issue was filed against.