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
- Return ['msgtype' => 'text', 'text' => ['content' => '...']] from JSON-mode handlers.
- Keep casing right: JSON mode wants lowercase msgtype; XML mode wants MsgType.
- 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
- Centralize reply builders per mode with the exact key casing
- Assert msgtype/MsgType in reply unit tests
- Return a PSR ResponseInterface for edge cases
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
- Invalid Response type "%s".
- Request ciphertext must not be empty.
- Invalid Response content "%s".
- MsgType cannot be empty.
- Invalid Response type "%s".
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/87bb0a315ddd6643.
Report an issue: GitHub.