Laravel × Guzzle でリクエストした時のエラーメッセージが (truncated...) で省略されて途中までしか読めない

```php:storage/logs/laravel.log
[2025-03-23 20:00:00] local.INFO: Exception message: Client error: `POST https://example.com/` resulted in a `422 Unprocessable Entity` response:
{"error":"RecordInvalid","description":"Record validation errors","details":{"email":[{"description":"メール: usernam (truncated...)
```

Laravel で Guzzle を使った API リクエスト時に ```Log::error($e->getMessage());``` を実行してログ出力すると、レスポンス（エラーメッセージ）が途中で ```truncated...``` となり省略され、全文はログに出力されないことがあった。今回はその原因と解決策をメモに残しておく。サンプルコードなど冗長な箇所もあると思うが取り急ぎ。

なお、（古くて恐縮だが）Laravelのバージョンは6。Guzzleは7だったと思う。エラーメッセージのサンプルはzendeskへリクエストした際のものを一部使用している。

結論から読む場合は[コチラ](https://qiita.com/chimayu/items/6b5f13768e1dda46e1b7#2-guzzle-%E3%81%AE-e-getmessage-%E3%81%AF%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E5%85%A8%E6%96%87%E3%82%92%E5%90%AB%E3%82%81%E3%81%AA%E3%81%84%E4%BB%95%E6%A7%98%E8%A7%A3%E6%B1%BA%E7%AD%96%E3%81%AE%E6%8F%90%E7%A4%BA)。

## 1. なぜ getMessage() が truncated されるか？

Guzzle の RequestException クラスに ```getMessage()``` メソッドは存在しない。```getMessage()``` は PHP の Exception クラスに元々備わっているメソッドであり、RequestException は Exception を拡張しているため、```getMessage()``` が直接定義されているわけではない。
そして Guzzle の RequestException クラスでは、 ```create()``` メソッドを使ってエラーメッセージを組み立てている。以下に一部抜を粋する。

```php:vendor/guzzlehttp/guzzle/src/Exception/RequestException
$message = \sprintf(
    '%s: `%s %s` resulted in a `%s %s` response',
    $label,
    $request->getMethod(),
    $uri->__toString(),
    $response->getStatusCode(),
    $response->getReasonPhrase()
);

$summary = ($bodySummarizer ?? new BodySummarizer())->summarize($response);

if ($summary !== null) {
    $message .= ":\n{$summary}\n";
}
```

```$summary``` の変数に格納されるメッセージの内容がキモで、Guzzle の内部処理として長すぎるレスポンスボディをカットしている可能性 があり、そのため長いエラーメッセージが途中で ```truncated...``` となることがあるようだ。

```$summary``` に格納しているデータの作り方を見てみよう。


```php:vendor/guzzlehttp/guzzle/src/BodySummarizer.php
public function summarize(MessageInterface $message): ?string
{
    return $this->truncateAt === null
        ? Psr7\Message::bodySummary($message)
        : Psr7\Message::bodySummary($message, $this->truncateAt);
}
```

```php:vendor/guzzlehttp/psr7/src/Message.php
public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string
{
    $body = $message->getBody();

    if (!$body->isSeekable() || !$body->isReadable()) {
        return null;
    }

    $size = $body->getSize();

    if ($size === 0) {
        return null;
    }

    $body->rewind();
    $summary = $body->read($truncateAt);
    $body->rewind();

    if ($size > $truncateAt) {
        $summary .= ' (truncated...)';
    }

    // Matches any printable character, including unicode characters:
    // letters, marks, numbers, punctuation, spacing, and separators.
    if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) {
        return null;
    }

    return $summary;
}
```

上記からわかるのは、```bodySummary()``` メソッドの引数として ```$truncateAt = 120``` がデフォルトで適用されており、メッセージのサイズが120を超えるものは ```$summary .= ' (truncated...)';``` の通り省略されている、ということ。
冒頭で示したログの以下部分をカウントしたら確かに120バイト（UTF-8）になっていた。

```
{"error":"RecordInvalid","description":"Record validation errors","details":{"email":[{"description":"メール: usernam
```

## 2. Guzzle の $e->getMessage() はレスポンス全文を含めない仕様（解決策の提示）

Guzzle の RequestException は、レスポンスボディの全文を ```getMessage()``` に含める設計ではないということがわかった。そのため、エラーメッセージとして取得できるのはレスポンスの一部のみとなる。

:::note info
解決策: getBody()->getContents() を使う
:::

エラーレスポンスの全文を取得したい場合は、```getMessage()``` ではなく ```getBody()->getContents()``` を使ってみる。

```php:サンプルコード
try {
    // Guzzle のリクエスト処理
} catch (\GuzzleHttp\Exception\RequestException $e) {
    $response = $e->getResponse();
    $message = $e->getMessage() ?? 'No message';
    $body = $response ? $response->getBody()->getContents() : 'No response body';
    
    Log::error("Error: " . $message); // truncated される可能性あり
    Log::error("Full Response: " . $body);    // truncated されない
}
```

```php:サンプルログ
[2025-03-23T20:00:00.000000+09:00] local.ERROR: Error: Client error: `POST https://example.com` resulted in a `422 Unprocessable Entity` response:
{"error":"RecordInvalid","description":"Record validation errors","details":{"email":[{"description":"メール: usernam (truncated...)

[2025-03-23T20:00:00.000000+09:00] local.ERROR: Full Response: {"error":"RecordInvalid","description":"Record validation errors","details":{"email":[{"description":"メール: username@example.com はすでに使われています","error":"DuplicateValue"}]}}  
```

## 3. ログ肥大化を防ぐための追加対策

レスポンスボディが非常に長い場合、ログ肥大化のリスクがある。そのため、以下の方法で対策するとより安全かもしれない。

### 3-1. getStatusCode() や getReasonPhrase() の活用

レスポンスの要点だけを知りたい場合は、```getStatusCode()``` や ```getReasonPhrase()``` を使ってみる。

```php
$response = $e->getResponse();
$status_code = $response ? $response->getStatusCode() : 'No code';
$reason = $response ? $response->getReasonPhrase() : 'No response';
Log::error("Error Reason: " . $status_code . ' ' . $reason);
```

```php:サンプルログ
[2025-03-23T20:00:00.000000+09:00] local.ERROR: Error Reason: 422 Unprocessable Entity  
```


### 3-2. getSize() でレスポンスサイズをチェック

ログのサイズを制御するために、レスポンスのサイズを取得して適切に処理することも可能。

```php
$size = $response ? $response->getBody()->getSize() : 0;
Log::error("Response Size: " . $size . " bytes");
```

```php:サンプルログ
[2025-03-23T20:00:00.000000+09:00] local.ERROR: Response Size: 210 bytes  
```

## 4. まとめ

✅ Guzzle の RequestException は、レスポンスボディの全文を ```getMessage()``` へ含める設計にはなっていない（デフォルトだと文字長の制限がある）
✅ ```getMessage()``` の代わりに ```getBody()->getContents()``` を使えば、
```truncated``` されずメッセージを全文取得できる
✅ ログ肥大化を防ぐため、```getStatusCode()```・```getReasonPhrase()```・```getSize()``` の活用も有効


#### 参考URL
[Guzzle Documentation](https://docs.guzzlephp.org/en/stable/)
[Guzzleについてちょっと調べた](https://qiita.com/mapyo/items/5307e1b36437600c2c34)