phalcon/cphalcon · error · Phalcon\Mvc\Router\Exceptions\WrongPathsKey

Wrong key in paths

Error message

Wrong key in paths: {part}

What it means

Thrown during router matching (combined-regex fast path, which only runs when no events manager is attached and the route bucket has no hostname constraints) when a matched route's paths array contains a key that is not a string. Route paths must be an associative map of parameter name => capture position (e.g. ['controller' => 1, 'action' => 2]); a plain list array has integer keys, so the router cannot resolve parameter names and rejects it at match time, not at add() time.

Solutions

  1. Change the route's second argument to an associative array: ['controller' => 'posts', 'action' => 'show', 'id' => 1] where numeric VALUES are regex capture positions
  2. If paths come from config or JSON, ensure keys are quoted strings ("controller": ...) so they survive decoding as strings
  3. Audit every $router->add/addGet/addPost/... call or config 'paths' entry feeding this route and replace list syntax with a name => position map
  4. As a stopgap to surface the bug earlier (outside production), iterate your routes at boot and assert all array keys of getPaths() are strings

Example fix

// before
$router->add(
    '/admin/:controller/:action/:params',
    ['controller', 'action', 'params']
);

// after
$router->add(
    '/admin/:controller/:action/:params',
    ['controller' => 1, 'action' => 2, 'params' => 3]
);
Defensive patterns

Strategy: validation

Validate before calling

// before registering routes, assert every paths array is a name => position map
function assertValidPaths(array $paths): void
{
    foreach ($paths as $key => $_) {
        if (!is_string($key)) {
            throw new InvalidArgumentException(
                'Route paths must use string keys, got key: ' . var_export($key, true)
            );
        }
    }
}

foreach ($routesToRegister as $i => $def) {
    assertValidPaths($def['paths'] ?? []);
    $router->add($def['pattern'], $def['paths']);
}

Type guard

function isValidPaths(mixed $paths): bool
{
    if (!is_array($paths)) {
        return false;
    }

    foreach (array_keys($paths) as $key) {
        if (!is_string($key)) {
            return false;
        }
    }

    return true;
}

Try / catch

try {
    $router->handle($uri);
} catch (\Phalcon\Mvc\Router\Exceptions\WrongPathsKey $e) {
    $logger->error('Bad route paths definition: ' . $e->getMessage());
    // fail the request explicitly; do not serve a half-matched route
    $response->setStatusCode(500)->send();
}

Prevention

When it happens

Trigger: Defining a route via $router->add('/api/users', ['controller', 'action']) (list instead of map) and then issuing a request that matches it; also paths built with array_values(), array_merge() into a list, or decoded from JSON that produced numeric keys. The throw happens inside Router::handle() only after the URI actually matches, so the defect can sit dormant until traffic hits the route.

Common situations: Copy-pasting a paths example but dropping the '=>' pairs; building paths dynamically from a loop that uses append ($paths[] = ...) instead of named keys; YAML/JSON route files where quotes were lost and keys became integers; refactoring from short string paths ('Posts::show') to arrays and using wrong syntax.

Related errors


AI-assisted analysis of phalcon/cphalcon@b7419de9cd (2026-08-21). Data as JSON: /api/errors/1d9788273a5bf986. Report an issue: GitHub.

Appendix: source

Thrown at phalcon/Mvc/Router.zep:1429

                        throw new BeforeMatchNotCallable();
                    }

                    if !{combinedBeforeMatch}(handledUri, combinedRoute, this) {
                        continue;
                    }
                }

                let combinedPaths      = combinedRoute->getPaths(),
                    parts              = combinedPaths,
                    matches            = combinedMatchesLocal,
                    combinedConverters = combinedRoute->getConverters(),
                    this->matches      = combinedMatchesLocal,
                    this->matchedRoute = combinedRoute,
                    routeFound         = true;

                for combinedPart, combinedPosition in combinedPaths {
                    if unlikely typeof combinedPart !== "string" {
                        throw new WrongPathsKey(combinedPart);
                    }

                    if typeof combinedPosition !== "string" && typeof combinedPosition !== "integer" {
                        continue;
                    }

                    if fetch combinedMatchPosition, combinedMatchesLocal[combinedPosition] {
                        if typeof combinedConverters === "array" && fetch combinedConverter, combinedConverters[combinedPart] {
                            let parts[combinedPart] = {combinedConverter}(combinedMatchPosition);
                            continue;
                        }

                        let parts[combinedPart] = combinedMatchPosition;
                    } else {
                        if typeof combinedConverters === "array" && fetch combinedConverter, combinedConverters[combinedPart] {
                            let parts[combinedPart] = {combinedConverter}(combinedPosition);
                        } elseif typeof combinedPosition === "integer" {
                            unset parts[combinedPart];

View on GitHub (pinned to b7419de9cd)