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
- Pass a supported form: Closure, 'Class::method', [new Class, 'method'], or an invokable class name.
- Null-check optional handlers before registering: if ($handler) { $server->withHandler($handler); }.
- Validate config-driven handler maps with is_callable()/class_exists() before withHandler().
- 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
- Default optional handler configs to a concrete invokable class
- Lint config-declared handlers at boot
- Build handlers in one factory so bad types surface early
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.