w7corp/easywechat · error · InvalidArgumentException

The type of `xml` must be string or array.

Error message

The type of `xml` must be string or array.

What it means

RequestUtil::formatBody() normalizes request options: an 'xml' option that is an array is converted via Xml::build, and if the result is not a string it throws InvalidArgumentException('The type of `xml` must be string or array.'). Only two shapes are accepted: a plain PHP array or an already-built XML string.

Source

Thrown at src/Kernel/HttpClient/RequestUtil.php:122

    }

    /**
     * @param  array{headers?:array<string, string>, xml?:mixed, body?:array|string, json?:mixed}  $options
     * @return array{headers?:array<string, string|array<string, string>|array<string>>, xml?:array|string, body?:array|string}
     *
     * @throws InvalidArgumentException
     */
    public static function formatBody(array $options): array
    {
        $contentType = $options['headers']['Content-Type'] ?? $options['headers']['content-type'] ?? null;

        if (array_key_exists('xml', $options)) {
            if (is_array($options['xml'])) {
                $options['xml'] = Xml::build($options['xml']);
            }

            if (! is_string($options['xml'])) {
                throw new InvalidArgumentException('The type of `xml` must be string or array.');
            }

            if (! $contentType) {
                $options['headers']['Content-Type'] = 'text/xml';
            }

            $options['body'] = $options['xml'];
            unset($options['xml']);
        }

        if (array_key_exists('json', $options)) {
            if (is_array($options['json'])) {
                /** XXX: 微信的 JSON 是比较奇葩的,比如菜单不能把中文 encode 为 unicode */
                $options['json'] = json_encode(
                    $options['json'],
                    empty($options['json']) ? JSON_FORCE_OBJECT : JSON_UNESCAPED_UNICODE
                );
            }

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Pass the XML payload as an array (recommended: the client builds it) or as a finished XML string
  2. Guard builders that may return null: default to [] before assigning to the 'xml' option
  3. Log the option type right before the request when this fires intermittently

Example fix

// before
$body = ['xml' => $this->buildOrder($order)]; // buildOrder() returned null

// after
$body = ['xml' => $this->buildOrder($order) ?? []];
Defensive patterns

Strategy: type-guard

Type guard

function isValidXmlOption(mixed $xml): bool
{
    return is_array($xml) || is_string($xml);
}

// before sending
if (! isValidXmlOption($options['xml'] ?? null)) {
    throw new InvalidArgumentException("xml option must be array|string, got ".gettype($options['xml']));
}

Try / catch

try {
    $response = $api->postJson('/pay/unifiedorder', $options);
} catch (\EasyWeChat\Kernel\Exceptions\InvalidArgumentException $e) {
    if (str_contains($e->getMessage(), '`xml`')) {
        $options['xml'] = $options['xml'] ?? [];
        $response = $api->postJson('/pay/unifiedorder', $options);
    }
}

Prevention

When it happens

Trigger: Calling a v2 Pay API client (XML-transport endpoints) with ['xml' => null] or ['xml' => 0] because an upstream builder returned null, or passing a SimpleXMLElement/object instead of an array or string.

Common situations: Optional payloads built conditionally and ending up null, refactoring that changes the builder's return type, feeding an object from another XML library into the option.

Related errors


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