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): static

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. If registering by class name, add public function __invoke(...) to the class.
  2. Fix the class string (correct namespace + spelling) and run composer dump-autoload.
  3. For instance or non-static methods, use a closure or [new Handler, 'method'].
  4. 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

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.