w7corp/easywechat · error · InvalidArgumentException
Invalid handler: %s.
Error message
Invalid handler: %s.
What it means
makeClosure() resolves a string handler: it must either be callable (a function name or 'Class::staticMethod') or name an existing, autoloadable class that has __invoke. Anything else throws this InvalidArgumentException containing the original string. This is the path used when registering handlers by class name, e.g. $server->withHandler(NotifyHandler::class) or ->withHandler('App\Handlers\TextHandler').
Source
Thrown at src/Kernel/Traits/InteractWithHandlers.php:98
/**
* @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
*/
return fn (): mixed => (new $handler)(...func_get_args());
}
throw new InvalidArgumentException(sprintf('Invalid handler: %s.', $handler));
}
public function prepend(callable|string $handler): static
{
return $this->prependHandler($handler);
}
public function prependHandler(callable|string $handler): static
{
array_unshift($this->handlers, $this->createHandlerItem($handler));
return $this;
}
/**
* @throws InvalidArgumentException
*/
public function without(callable|string $handler): staticView on GitHub (pinned to f0cf0a8b83)
Solutions
- If registering by class name, add public function __invoke(...) to the class.
- Fix the class string (correct namespace + spelling) and run composer dump-autoload.
- For instance or non-static methods, use a closure or [new Handler, 'method'].
- Replace 'Class@method' syntax with a closure or a real invokable class.
Example fix
// before: class has no __invoke
$server->withHandler(NotifyHandler::class);
// after: make the handler invokable
final class NotifyHandler {
public function __invoke(Message $message, Closure $next): mixed { return $next($message); }
}
$server->withHandler(NotifyHandler::class); Defensive patterns
Strategy: validation
Validate before calling
if (!is_callable($h) && !(is_string($h) && class_exists($h) && method_exists($h, '__invoke'))) { throw new InvalidArgumentException('Unsupported handler string: '.$h); }
$server->withHandler($h); Type guard
function isInvokableHandler(string $h): bool { return is_callable($h) || (class_exists($h) && method_exists($h, '__invoke')); } Try / catch
try { $server->withHandler($handler); } catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) { throw new RuntimeException('Handler "'.$handler.'" is neither callable nor an invokable class (typo? missing __invoke? stale autoload?)', 0, $e); } Prevention
- Give every handler class a public __invoke by convention
- Run composer dump-autoload after adding handler classes
- Cover handler registration in a boot smoke test
When it happens
Trigger: withHandler(SomeHandler::class) where SomeHandler has no __invoke method; a typo'd class name ('TextHander'); the class exists but is not autoloadable in the runtime (stale composer autoload); Laravel-style 'App\Handler@handle' strings, which this SDK does not support.
Common situations: Copy-pasting handler registration from another app with different namespaces; new handler classes created but composer dump-autoload not run; refactors renaming classes without updating string-based registrations.
Related errors
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/05a93e8c7a5049cf.
Report an issue: GitHub.