AWS-SDK-PHP で Header value cannot be represented using ISO-8859-1. エラーの解決方法

## きっかけ

 S3 の署名付 URL を使用してオブジェクトをダウンロードする際、response-content-disposition の filename が日本語だと、タイトルに記載されているエラーが発生していました。
 
 自己解決の過程で、ファイルダウンロード周りの使用や文字コード周りの知見を増やすことができたので、備忘録的に記事にまとめたいと思います。

## エラー発生時の実装

下記のような実装でした。

```php
'ResponseContentDisposition' => 'attachment; filename=' . $fileName
```

## 解決後の実装

以下のような実装にすることで日本語のままダウンロードすることができました。

```php
$fileNameEncoded = urlencode($fileName);

$cmd = $this->inner->getCommand('GetObject', [
	// ~中略~
	'ResponseContentDisposition' => "attachment; filename*=UTF-8''{$fileNameEncoded}"
]);
```


filename の末尾に `*` を追加し、ファイル名の先頭に `UTF-8''` を追加しました。
それとファイル名を URL エンコードすることで無事解決することができました。

## 解説
### Content-Disposition の仕様
まず、`filename*` の役割を理解するために RFC 6226 に定義されてある `Content-Disposition` ヘッダーフィールドの仕様を見てみます。セクション 4.3 からの抜粋です。

> パラメータ「filename」および「filename*」は、大文字と小文字を区別せずに照合され、メッセージ ペイロードを保存するためのファイル名の作成方法に関する情報を提供します。

> パラメータ "filename" と "filename*" の違いは、"filename*" が [RFC5987] で定義されているエンコーディングを使用し、ISO-8859-1 文字セット ([ISO-8859-1]) に存在しない文字の使用を許可することだけです。 ]）。

`filename*` は RFC 5987 で定義されているエンコーディング方式を使用することで、ISO-8859-1 以外の文字セットを表現することができる。と記載されていますね。

この説明文を読んだときに、基本的に HTTP ヘッダーフィールドの文字コードは US-ASCII であるはずでは？と思ったので、当時の HTTP 文字セットとエンコーディングに関する RFC 5987 をみてたところ、下記のような記載がありました。

> デフォルトでは、HTTP（[[RFC2616]](https://tex2e.github.io/rfc-translater/html/rfc2616.html)）メッセージのメッセージヘッダーフィールドパラメーターは、ISO-8859-1 文字セット（[ISO-8859-1]）の外側に文字を運ぶことができません。

おそらく、策定当時の HTTP ヘッダーフィールドの標準文字コードは仕様上 ISO-8859-1 文字セットと定義されていたのではと思います。

しかし、現在では、RFC 5987 は廃止され HTTP の文字セットとエンコーディングに関する RFC は RFC8187 が最新となっています。RFC 8187 の仕様によると、

> HTTP ヘッダー フィールド ([RFC7230]) での US-ASCII コード化文字セット ([RFC0020]) 以外の文字の使用は、簡単ではありません。

と記載されています。なので `filename*` に限らずですが、HTTP ヘッダーフィールドの文字コードは大体の場合 US-ASCII であり、RFC 5987 で ISO-8859-1 と記載されてあるのは、おそらく、RFC が策定された時点で HTTP/1.1 の仕様で定められている文字コードが ISO-8859-1 だったからだと思われます。

なので、これはあくまで推測ですが、歴史的背景を加味してエラー文には「Header value cannot be represented using ISO-8859-1.」としてあるが、実際に指定できる文字セットは US-ASCII のみとなっているというのが本当のところではと思います。

また、`UTF-8''` とすることで「UTF-8 文字コード」としてファイルをダウンロードすることができます。セクション５の Examples をみてみましょう。

```
Content-Disposition: attachment; filename*= UTF-8''%e2%82%ac%20rates
```

Chrome では `UTF-8''` と指定せずとも、自動で URL デコードし、日本語名のままダウンロードできたのですが、Safari はエンコードされたままダウンロードされたので、この記述に関してはブラウザ間の差異を埋める意味でも指定しておいた方が無難だと思います。

### URL エンコードする理由
最後に、ファイル名を `urlencode` する理由ですが、これは S3 が HTTP レスポンスの `Content-Disposition` ヘッダーフィールドにクエリ文字列として渡ってきた `response-content-disposition` の値を URL デコードした後にセットするからです。

URL デコードした `filename*` が日本語名だと、ヘッダーフィールドが非 ASCII とみなされタイトルのようなエラーが発生します。

正常にダウンロードされるまでのフローを記したいと思います。
ファイル名を猫.jpg と仮定します。

1. 署名付き URL によって URL エンコードされ、`response-content-disposition` がクエリ文字列として付与される。
    ```
    https://bucket.ap-northeast-1.amazonaws.com/some/key/%E7%8C%AB.pdf?response-content-disposition=attachment%3B%20filename%2A%3DUTF8%27%27%25E7%258C%25AB.pdf
    ```
    - `key` は URL エンコードした文字列ですが、`response-content-disposition` は二重エンコードしたものとなっています。

2. S3 側で受け取った後、デコードする
	- %E7%8C%AB.jpg `→` 猫.jpg
	- attachment%3B%20filename%2A%3DUTF8%27%27%25E7%258C%25AB.pdf `→` attachment; filename*=UTF8''%E7%8C%AB.pdf
3. `Content-Disposition` ヘッダーフィールドに付与する
	- `Content-Disposition=attachment; filename*=UTF8''%E7%8C%AB.pdf`
4. レスポンスが返却され、正常にダウンロードされる

つまり URL エンコードした `filename` を署名付き URL 生成時に URL エンコードしているので実質二重エンコードしているのですが、そうしないと、タイトルのエラーが発生してしまいます。

### まとめ
今回調査する中で、RFCを読み漁ったり、文字コードの関する知識を体系的にインプットでき貴重な学びになりました。
この記事が誰かの役に立てたのであれば幸いです。