PHP loose comparison turned an empty endpoint into a 500
A backend I run was returning 500s on GET /api.php with no
endpoint= query param. Should obviously be a 400. Tracking it
down landed me on one of those classic PHP foot-guns that
modern style guides keep telling you to avoid for exactly this
reason.
What was happening
The router used a switch ($endpoint) block with regex-pattern
cases written as:
switch ($endpoint) {
case (preg_match('#^weather/alert/(\d+)/dismiss$#', $endpoint, $matches) ? true : false):
// handle dismiss
break;
}
The intent: if the regex matches, enter this case. The reality:
when $endpoint was the empty string, "" == false is true in
PHP. The case matched, the handler ran, $matches[1] was
undefined, and the DynamoDB query went out with a null id. That
raised a ValidationException and the response was a 500
instead of a nice 400 "Missing endpoint parameter."
What I found
Two separate problems sitting on top of each other:
- No empty-endpoint guard in front of the router. The router trusted that something would always be there.
- There was actually a duplicate handler for
weather/alert/{id}/dismiss— one inapi.phpusing thecase (preg_match(...) ? true : false)pattern, one inapi_extensions.phpusing a cleanif (preg_match(...))guard. Routing fell into the broken one first.
The broken one was the kind of code you write once because "switch is faster than if/elseif" and then you regret forever because it interacts with PHP's loose comparison rules in ways the next person to read it can't predict.
The fix
Two changes in api.php:
// Guard at the top of the router
if ($endpoint === null || $endpoint === '') {
http_response_code(400);
echo json_encode(['error' => 'Missing endpoint parameter']);
return;
}
// Then: delete the duplicate case-with-preg_match block entirely.
// The clean handler in api_extensions.php already exists and uses:
//
// if (preg_match('#^weather/alert/(\d+)/dismiss$#', $endpoint, $matches)) {
// handleDismissWeatherAlert($matches[1]);
// return;
// }
Verification post-deploy:
GET /api.php → 400 (was 500)
POST /api.php?endpoint=weather/alert/999/dismiss → 404 (was 500)
GET /api.php?endpoint=smarthome/devices → 200 (regression)
What I'd do differently
case (preg_match(...) ? true : false): is one of those things
that looks clever until somebody passes you an empty string and
your switch starts catching everything truthy or everything
falsy at random. Use if/elseif for regex routing, or use a
router library, or at minimum write === with strict comparison.
The duplicate handler in two files is the bigger smell. When the
clean version was added to api_extensions.php somebody should
have ripped out the broken one in api.php. Cleanup work after a
refactor matters; leaving the old code "in case we need it" is
how you accumulate booby-traps.