Back to blog
FILE 0x23·PHP LOOSE COMPARISON TURNED AN EMPTY ENDPOINT INTO A 500

PHP loose comparison turned an empty endpoint into a 500

April 24, 2026 · php, debugging, aws, lambda

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:

  1. No empty-endpoint guard in front of the router. The router trusted that something would always be there.
  2. There was actually a duplicate handler for weather/alert/{id}/dismiss — one in api.php using the case (preg_match(...) ? true : false) pattern, one in api_extensions.php using a clean if (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.