w7corp/easywechat · error · InvalidArgumentException

msgtype cannot be empty.

Error message

msgtype cannot be empty.

What it means

Work Server can answer pushes with a JSON passive reply via transformJsonToReply (used when messageType is 'json', src/Work/Server.php:58-60). normalizeJsonResponse() accepts only arrays and requires the lowercase 'msgtype' key — the case-sensitive isset($response['msgtype']) — mirroring WeChat Work's customer-service JSON reply shape. Returning an array without it throws InvalidArgumentException before any JSON is built.

Source

Thrown at src/Kernel/Traits/RespondJsonMessage.php:37

        return $this->createJsonResponse(
            attributes: $this->normalizeJsonResponse($response),
            encryptor: $encryptor
        );
    }

    /**
     * @throws InvalidArgumentException
     */
    protected function normalizeJsonResponse(mixed $response): array
    {
        if (! is_string($response) && is_callable($response)) {
            $response = $response();
        }

        if (is_array($response)) {
            if (! isset($response['msgtype'])) {
                throw new InvalidArgumentException('msgtype cannot be empty.');
            }

            return $response;
        }

        throw new InvalidArgumentException(
            sprintf('Invalid Response type "%s".', gettype($response))
        );
    }

    /**
     * @throws RuntimeException
     */
    protected function createJsonResponse(array $attributes, ?Encryptor $encryptor = null): ResponseInterface
    {
        $jsonStr = json_encode($attributes, JSON_UNESCAPED_UNICODE);

        if (is_string($jsonStr)) {

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Return ['msgtype' => 'text', 'text' => ['content' => '...']] from JSON-mode handlers.
  2. Keep casing right: JSON mode wants lowercase msgtype; XML mode wants MsgType.
  3. To send no reply, return '' or null (the trait answers 'success' itself) or a PSR ResponseInterface.

Example fix

// before: Work JSON mode, array without msgtype
return ['Content' => 'hello'];
// after: required lowercase msgtype plus the message body
return ['msgtype' => 'text', 'text' => ['content' => 'hello']];
Defensive patterns

Strategy: validation

Validate before calling

if (!isset($reply['msgtype'])) { $reply = ['msgtype' => 'text', 'text' => ['content' => (string) ($reply['Content'] ?? '')]]; }

Type guard

function isWorkJsonReply(mixed $r): bool { return is_array($r) && isset($r['msgtype']); }

Try / catch

try { return $server->serve(); } catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) { if (str_contains($e->getMessage(), 'msgtype')) { \Log::error('Work JSON reply missing msgtype'); return new \Nyholm\Psr7\Response(200, [], 'success'); } throw $e; }

Prevention

When it happens

Trigger: A Work JSON-mode handler returns ['text' => ['content' => 'hi']] without msgtype; returns ['MsgType' => 'text'] (uppercase only works in XML mode); returns a payload shaped for the async customer-service REST API instead of a passive reply.

Common situations: Porting code between the customer-service REST API (msgtype+text) and passive JSON replies; one codebase serving both Work JSON mode and OfficialAccount XML mode with different key casing ('msgtype' vs 'MsgType').

Related errors


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