w7corp/easywechat · error · InvalidArgumentException

Invalid handler: %s

Error message

Invalid handler: %s

What it means

getHandlerHash() builds a dedup key for handlers registered via withHandler()/addMessageListener() etc. Strings map to themselves, [class, method] arrays to Class::method, closures and callables to spl_object_hash; anything else (int, float, bool, null, non-callable object — gettype() is appended to the message) hits the default branch and throws InvalidArgumentException. Public entry points type-hint callable|string, so in plain SDK usage PHP raises a TypeError first; this branch mainly fires in subclasses that loosen the signature or in direct getHandlerHash() calls.

Source

Thrown at src/Kernel/Traits/InteractWithHandlers.php:76

        return [
            'hash' => $this->getHandlerHash($handler),
            'handler' => $this->makeClosure($handler),
        ];
    }

    /**
     * @throws InvalidArgumentException
     */
    protected function getHandlerHash(callable|array|string $handler): string
    {
        return match (true) {
            is_string($handler) => $handler,
            is_array($handler) => is_string($handler[0])
                ? $handler[0].'::'.$handler[1]
                : get_class($handler[0]).$handler[1],
            $handler instanceof Closure => spl_object_hash($handler),
            is_callable($handler) => spl_object_hash($handler),
            default => throw new InvalidArgumentException('Invalid handler: '.gettype($handler)),
        };
    }

    /**
     * @throws InvalidArgumentException
     */
    protected function makeClosure(callable|string $handler): callable
    {
        if (is_callable($handler)) {
            return $handler;
        }

        if (class_exists($handler) && method_exists($handler, '__invoke')) {
            /**
             * @psalm-suppress InvalidFunctionCall
             *
             * @phpstan-ignore-next-line https://github.com/phpstan/phpstan/issues/5867
             */

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Pass a supported form: Closure, 'Class::method', [new Class, 'method'], or an invokable class name.
  2. Null-check optional handlers before registering: if ($handler) { $server->withHandler($handler); }.
  3. Validate config-driven handler maps with is_callable()/class_exists() before withHandler().
  4. If you subclass Server, keep the callable|string signature so PHP rejects bad types early.

Example fix

// before: optional handler that may be null
$server->withHandler($config['fallback_handler']);
// after: guard before registering
if ($handler = $config['fallback_handler'] ?? null) {
    $server->withHandler($handler);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if ($handler !== null && (is_callable($handler) || (is_string($handler) && class_exists($handler)))) { $server->withHandler($handler); }

Type guard

function isRegistrableHandler(mixed $h): bool { return $h instanceof \Closure || (is_string($h) && $h !== '') || (is_array($h) && isset($h[0], $h[1])) || (is_object($h) && is_callable($h)); }

Try / catch

try { $server->withHandler($handler); } catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) { if (str_starts_with($e->getMessage(), 'Invalid handler')) { throw new InvalidArgumentException('Handler map entry rejected: '.get_debug_type($handler), 0, $e); } throw $e; }

Prevention

When it happens

Trigger: Calling getHandlerHash() directly with null/int/bool/non-callable objects; a subclass widening withHandler() to mixed and passing an uninitialized handler variable; data-driven handler maps where a config key is absent so null is registered.

Common situations: Optional handler variables defaulting to null; config-driven handler lists with missing keys; wrapping handlers in plain DTOs without __invoke.

Related errors


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/8997a4018593244e. Report an issue: GitHub.