この記事では、HTTPレスポンスとしてファイルをダウンロードさせたいときのheaderの指定方法について調べたことをまとめています。これまでなんとなく記述していたContent-TypeやContent-Dispositionについて触れています。
結論から書くと、ファイルをダウンロードさせたいときには次のようにheaderを書くと良さそうです。
- Content-Typeに適切なファイル種別を入れる
- Content-Dispositionをattachmentにする
私の理解では上記のような結論になりましたが、間違いなどありましたらご指摘いただけると大変嬉しいです。
JSONファイルがブラウザに表示されずにダウンロードされる
この記事を書くきっかけになったのが、下記のコードです。
index.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のファイル種別をタイプミスしていたことが上記の挙動の原因でした。
- 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
の場合も、一般的には次のようにブラウザに表示されます。
※ 画像はChromeのJSON Formatterという拡張機能で整形しています
しかし、このContent-Typeに未知のファイル種別が指定されていると、ブラウザ側の仕様でファイルをダウンロードさせてしまうことが多いようです。つまり、Content-Type: applicarion/json
とタイプミスした影響で、ブラウザに未知のファイル種別と誤認されてしまっていたようです。
ファイルをダウンロードさせるときのheaderの書き方
ファイルをダウンロードさせたい場合、上記のようにContent-Typeに未知のファイル種別を指定する以外にも、いくつか方法があるようです。参考にさせていただいたこちらの記事では、次のように書かれていました。
ダウンロード形式に関わるヘッダは、
Content-Type
Content-Disposition
の2つがあり、調べていると以下の3つの指定がよく使われているのが見つかります。
- Content-Type: application/force-download
- Content-Type: application/octet-stream
- Content-Disposition: attachment
Content-Typeをapplication/force-downloadにする方法
ファイルをダウンロードさせたいとき、Content-Type: application/force-download
という指定の仕方は慣習的によく使われるようですが、実は**application/force-download
というMIMEタイプ(ファイル種別)は存在しない**ようです。
つまり、さきほど私がファイル種別をタイプミスしたときと同様に、未知のMIMEタイプが指定されたときはファイルをダウンロードする挙動になる、というブラウザの仕様を利用したダウンロードに過ぎないようです。application/force-download
でなく、hoge
など任意の文字列をContent-Typeに指定してもファイルはダウンロードされました。
// ファイルがダウンロードされる
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
ヘッダーの値にattachment
が設定されたかのように扱い、「名前を付けて保存」ダイアログを提案します。https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types
さきほどのapplication/force-download
は公式には存在しないMIMEタイプでしたが、こちらのapplication/octet-stream
は公式に定義されたMIMEタイプのようです。
// ファイルがダウンロードされる
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というパラメータでダウンロードファイルの名前のデフォルト値を設定することも可能です。
// ファイルがダウンロードされる
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
echo "hello, world.";
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/html; charset=UTF-8 // HTMLファイルとして出力
ウェブページとして表示されました。
Content-Type: text/plainを指定した場合
index.php
<?php
header("Content-Type: text/plain");
echo "hello, world.";
-> % curl --http1.1 --get -v http://localhost/index.php
~~ 中略 ~~
< Content-Type: text/plain;charset=UTF-8 // テキストファイルとして出力
ウェブページとして表示されましたが、CSSスタイルに少し違いがありました。
word-wrapプロパティ
word-wrapプロパティは、W3Cで審議中の仕様をInternet Explorerが独自に採用したもので、 表示範囲内に収まりきらない単語がある場合に、単語の途中で改行するかどうかを指定するに使用します
white-spaceプロパティ
white-spaceプロパティは、
1.ソース中のホワイトスペース(連続する半角スペース・タブ)の表示方法
2.ソース中の改行の表示方法
の2点を指定するプロパティです。 この2つの表示方法の組み合わせパターンの数だけ値が用意されている、と考えると理解しやすいかもしれません。~~ 中略 ~~
pre-wrap
ソース中のホワイトスペースをそのまま表示
ソース中の改行をそのまま表示
ボックスサイズが指定されている場合にはそれに合わせて自動改行する
Content-Type: application/octet-streamを指定する場合
index.php
<?php
header("Content-Type: application/octet-stream");
echo "hello, world.";
-> % 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
header('Content-Disposition: attachment; filename="hello.txt"');
echo "hello, world.";
-> % 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
header('Content-Disposition: attachment; filename="hello.txt"');
header("Content-Type: text/plain");
echo "hello, world.";
-> % 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でファイルの処理方法を指定するのが本来の分担のようです。