Content-Typeとattachmentを指定してファイルダウンロードする方法[PHP]

この記事では、HTTPレスポンスとしてファイルをダウンロードさせたいときのheaderの指定方法について調べたことをまとめています。これまでなんとなく記述していたContent-TypeやContent-Dispositionについて触れています。

結論から書くと、ファイルをダウンロードさせたいときには次のようにheaderを書くと良さそうです。

* Content-Typeに適切なファイル種別を入れる
* Content-Dispositionをattachmentにする

私の理解では上記のような結論になりましたが、間違いなどありましたらご指摘いただけると大変嬉しいです。

## JSONファイルがブラウザに表示されずにダウンロードされる

この記事を書くきっかけになったのが、下記のコードです。

`index.php`

```php
<?php

$items = [
    ['name' => 'banana', 'price' => 200],
    ['name' => 'orange', 'price' => 150],
];

// jsonに変換
$data = json_encode($items, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Content-Typeをjsonに指定
header("Content-Type: applicarion/json");

echo $data;
```

ブラウザから上記のファイルにアクセスすると、なぜかテキストファイルがダウンロードされてしまいました。

原因がしばらくわからなかったので、この機会にheaderのContent-Typeについてちゃんと知ろうと思ったのが、この記事を書いたきっかけです。

### テキストファイルがダウンロードされてしまう原因

結論から書くと、Content-Typeのファイル種別をタイプミスしていたことが上記の挙動の原因でした。

```php
- header("Content-Type: applicarion/json"); // 修正前 ファイルがダウンロードされる
+ header("Content-Type: application/json"); // 修正後 ブラウザに表示される

echo $data;
```

`application`の`t`が`r`になっており、そのせいでファイルがダウンロードされてしまっていました。

のちほど詳しく書きますが、Content-Typeはファイルの種別を表す項目らしく、ここに`text/html`や`application/pdf`など、ブラウザで表示可能なファイルが指定されている場合は表示するのが正常な挙動のようです。Content-Typeが`application/json`の場合も、一般的には次のようにブラウザに表示されます。

![スクリーンショット 2020-10-11 12.42.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500854/f1d27687-f5e2-aeed-18c1-c59d1f78a8d4.png)


※ 画像はChromeのJSON Formatterという拡張機能で整形しています

しかし、このContent-Typeに未知のファイル種別が指定されていると、ブラウザ側の仕様でファイルをダウンロードさせてしまうことが多いようです。つまり、`Content-Type: applicarion/json`とタイプミスした影響で、ブラウザに未知のファイル種別と誤認されてしまっていたようです。

## ファイルをダウンロードさせるときのheaderの書き方

ファイルをダウンロードさせたい場合、上記のようにContent-Typeに未知のファイル種別を指定する以外にも、いくつか方法があるようです。参考にさせていただいた[こちら](https://shkn.hatenablog.com/entry/2019/03/22/235503)の記事では、次のように書かれていました。

> ダウンロード形式に関わるヘッダは、 `Content-Type` `Content-Disposition` の２つがあり、調べていると以下の３つの指定がよく使われているのが見つかります。
>
> - Content-Type: application/force-download
> - Content-Type: application/octet-stream
> - Content-Disposition: attachment
>
> https://shkn.hatenablog.com/entry/2019/03/22/235503

### Content-Typeをapplication/force-downloadにする方法

ファイルをダウンロードさせたいとき、`Content-Type: application/force-download`という指定の仕方は慣習的によく使われるようですが、実は**`application/force-download`というMIMEタイプ（ファイル種別）は存在しない**ようです。

つまり、さきほど私がファイル種別をタイプミスしたときと同様に、**未知のMIMEタイプが指定されたときはファイルをダウンロードする挙動になる**、というブラウザの仕様を利用したダウンロードに過ぎないようです。`application/force-download`でなく、`hoge`など任意の文字列をContent-Typeに指定してもファイルはダウンロードされました。

```php
// ファイルがダウンロードされる
header("Content-Type: application/force-download");

echo $data;
```

この方法でダウンロード処理を実現することは可能なのですが、本来はMIMEタイプを記載すべき場所をダウンロード処理のために使っているという点に違和感を覚えました。

後述するContent-Dispositionは、ファイルをWEBページとして表示するか、ダウンロードさせるかを指定するためのheaderなので、こちらを使用するほうが自然なように思えました。

### Content-Typeをapplication/octet-streamにする方法

`application/octet-stream`は、未知のファイルを表すMIMEタイプのようです。

> application/octet-stream
>
> これは、バイナリファイルでは既定です。これは*未知のバイナリ形式*のファイルを表すものであり、ブラウザーはふつう実行したり、実行するべきか確認したりしません。これらは [`Content-Disposition`](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition) ヘッダーの値に `attachment` が設定されたかのように扱い、「名前を付けて保存」ダイアログを提案します。
>
> https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types

さきほどの`application/force-download`は公式には存在しないMIMEタイプでしたが、こちらの`application/octet-stream`は公式に定義されたMIMEタイプのようです。

```php
// ファイルがダウンロードされる
header("Content-Type: application/octet-stream");

echo $data;
```

`application/octet-stream`は公式なMIMEタイプらしいので、上記のような記述は間違いではないと思いますが、しかし、たとえばJSONファイルをダウンロードさせたいとき、MIMEタイプは`application/json`とわかっているのに、ダウンロードのためだけに`application/octet-stream`と書くのはやはり不自然な感じがします。

### Content-Dispositionをattachmentにする方法

Content-Dispositionは、ファイルをWEBページとして表示するか、ダウンロードさせるかを指定するためのheaderです。

| 値                   | 挙動                                                         |
| -------------------- | ------------------------------------------------------------ |
| inline（デフォルト） | ウェブページとして表示する                                   |
| attachment           | ファイルをダウンロードする（「名前を付けて保存」ダイアログを表示する） |

また、filenameというパラメータでダウンロードファイルの名前のデフォルト値を設定することも可能です。

```php
// ファイルがダウンロードされる
header('Content-Disposition: attachment; filename="任意のファイル名.json"');

echo $data;
```

Content-Dispositionは、もともとファイルの扱いを指定するためのheaderなので、これを利用してダウンロード処理を実現するのが最も自然に思えます。Content-Typeと併用も可能なので、正しいMIMEタイプを通知しつつ、ダウンロードさせることができます。

>ブラウザーの互換性
>
>|                       |    Chrome    |    Edge    |   Firefox    | Internet Explorer |    Opera     |    Safari    | Android webview | Android 版 Chrome | Android 版 Firefox | Android 版 Opera | iOSのSafari  | Samsung Internet |
>| :-------------------- | :----------: | :--------: | :----------: | :---------------: | :----------: | :----------: | :-------------: | :---------------: | :----------------: | :--------------: | :----------: | ---------------- |
>| `Content-Disposition` | 完全対応あり | 完全対応12 | 完全対応あり |   完全対応あり    | 完全対応あり | 完全対応あり |  完全対応あり   |   完全対応あり    |    完全対応あり    |   完全対応あり   | 完全対応あり | 完全対応あり     |
>
>https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Content-Disposition

上記を見た限り、互換性も問題なさそうです。

## curlコマンドで挙動の違いを確認

これまで紹介したContent-TypeやContent-Dispositionなどのheaderを変更しながら、レスポンスがどのように変化するのかを確かめてみました。確認にはcurlコマンドを使いました。

ちなみに、curlは「シーユーアールエル」とも読まれますが、公式に「カール」という発音が書かれているようです。

### 普通にechoした場合

`index.php`

```php
<?php

echo "hello, world.";
```

```bash
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/html; charset=UTF-8 // HTMLファイルとして出力
```

![スクリーンショット 2020-10-11 12.00.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500854/978a2329-9fef-6626-c3b3-b16da749afd7.png)


ウェブページとして表示されました。

### Content-Type: text/plainを指定した場合

`index.php`

```php
<?php

header("Content-Type: text/plain");
echo "hello, world.";
```

```bash
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/plain;charset=UTF-8 // テキストファイルとして出力
```

![スクリーンショット 2020-10-11 12.00.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/500854/96628dad-d07c-59a8-8c9a-dfd2ac5ecf78.png)


ウェブページとして表示されましたが、CSSスタイルに少し違いがありました。

**word-wrapプロパティ**

> word-wrapプロパティは、W3Cで審議中の仕様をInternet Explorerが独自に採用したもので、 表示範囲内に収まりきらない単語がある場合に、単語の途中で改行するかどうかを指定するに使用します
>
> http://www.htmq.com/style/word-wrap.shtml

**white-spaceプロパティ**

> white-spaceプロパティは、
> １.ソース中のホワイトスペース（連続する半角スペース・タブ）の表示方法
> ２.ソース中の改行の表示方法
> の２点を指定するプロパティです。 この２つの表示方法の組み合わせパターンの数だけ値が用意されている、と考えると理解しやすいかもしれません。
>
> ~~ 中略 ~~
>
> **pre-wrap**
>
> ソース中のホワイトスペースをそのまま表示
> ソース中の改行をそのまま表示
> ボックスサイズが指定されている場合にはそれに合わせて自動改行する
>
> http://www.htmq.com/style/white-space.shtml

### Content-Type: application/octet-streamを指定する場合





`index.php`

```php
<?php

header("Content-Type: application/octet-stream");
echo "hello, world.";
```

```bash
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: application/octet-stream // index.phpというファイル名でダウンロードされる
```



Chromeブラウザで確かめた結果、index.phpというファイル名でダウンロードされました。

### Content-Disposition: attachmentを指定する場合

`index.php`

```php
<?php

header('Content-Disposition: attachment; filename="hello.txt"');
echo "hello, world.";
```

```bash
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/html; charset=UTF-8 // HTMLファイルとして認識される
~~ 中略 ~~
< Content-Disposition: attachment; filename="hello.txt" // 指定されたファイル名でダウンロード
```

`Content-Type: application/octet-stream`を利用したときは`index.php`というファイル名でダウンロードされましたが、`Content-Disposition: attachment`を利用したときは、ちゃんと`hello.txt`という指定したファイル名でダウンロードされることが確認できました。

しかし、細かいことかもしれませんが、`Content-Type: text/html`と認識されている点が気になりました。

### Content-TypeとContent-Dispositionの両方を指定する場合

`index.php`

```php
<?php

header('Content-Disposition: attachment; filename="hello.txt"');
header("Content-Type: text/plain");
echo "hello, world.";
```

```bash
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/plain;charset=UTF-8 // プレーンテキストとして認識される
~~ 中略 ~~
< Content-Disposition: attachment; filename="hello.txt"
```

このようにContent-TypeとContent-Dispositionの両方を指定すると、正しいMIMEタイプが認識され、かつダウンロード処理やファイル名のデフォルト値も意図した通りに実現できました。

少し面倒ですが、ファイルをダウンロードさせたいときはこのように記述するのが良さそうです。

## まとめ

この記事で書いたことをまとめると次のようになります。

- Content-Typeをタイプミスすると表示可能なテキストファイルでもダウンロードされる
- ブラウザは未知のMIMEタイプを認識すると実行せずにファイルをダウンロードする
- ファイルをダウンロードさせたいときはContent-Typeに`application/force-download`や`application/octet-stream`がよく使われる
- Content-Dispositionをattachmentにすると任意のファイル名でダウンロードさせることができる

ファイルをダウンロードさせたいとき、Content-Typeを利用する方法とContent-Dispositionを利用する方法、どちらでも実現できますが、Content-TypeでMIMEタイプを指定し、Content-Dispositionでファイルの処理方法を指定するのが本来の分担のようです。