Content-Dispositionとは?ファイルダウンロードとファイル名について調べてみた
はじめに
ファイルダウンロードの仕組みを自作しているときに、Content-Disposition というHTTPヘッダーが出てきた。
最初は、
「ファイルをダウンロードさせるために必要なもの」
くらいの認識だった。
ところが、実際に調べてみると、いくつか気になることが出てきた。
Content-Type もファイルの種類を表しているのに、なぜ Content-Disposition も必要なのか?
また、
Content-Dispositionがなくてもファイルを取得できるのかinlineとattachmentは何が違うのかfilenameを指定するとChromeではどう動くのか- 日本語のファイル名はどう指定するのか
filenameとfilename*は何が違うのか- 両方指定した場合はどちらが使われるのか
といった疑問も出てきた。
そこで今回は、Node.jsで簡単なHTTPサーバーを作り、Chromeで実際の挙動を確認しながら調べてみた。
この記事の対象読者
この記事は、以下のような人を対象にしている。
Content-Dispositionという言葉を初めて知った人- HTTPでファイルダウンロードを実装している人
Content-TypeとContent-Dispositionの違いが分からない人filenameとfilename*の違いを知りたい人- ブラウザがダウンロードファイルの名前をどう決めるのか知りたい人
まず結論
今回調べてみて、最初に考えていた、
「ファイルをダウンロードさせるには
Content-Dispositionが必要」
という理解は、少し違っていた。
Content-Disposition は、単純に「ファイルをダウンロードさせるためだけ」のヘッダーではない。
HTTPレスポンスでは、たとえば次のような指定ができる。
Content-Disposition: inlineContent-Disposition: attachmentinline はコンテンツを通常の表示方法で扱うことを示し、attachment はダウンロードする添付ファイルとして扱うことを示す。さらに、filename や filename* を指定することで、保存時などに使うファイル名についても情報を伝えられる。RFC 6266では、これらの用途が定義されている。
一方、Content-Type は、そのデータがどのようなメディアタイプなのかを示すヘッダー。
そのため、ざっくり整理すると次のようになる。
| ヘッダー | 主な役割 |
|---|---|
Content-Type | データの種類を示す |
Content-Disposition | レスポンスをどう扱うかを示す |
filename | 保存時などに利用するファイル名を示す |
filename* | 文字コードを指定した形式でファイル名を示す |
filename* は拡張パラメータ形式を使って値の文字コードを指定でき、RFC 8187ではUTF-8をサポートすることが定義されている。
ただし、仕様としての定義と、実際のブラウザの挙動が常に同じとは限らない。
そのため、今回はChromeで実際に確認してみる。
Content-Dispositionとは?
基本的な定義
HTTPのレスポンスにおける Content-Disposition は、レスポンスのコンテンツをブラウザ内で通常どおり扱うのか、それとも添付ファイルとしてダウンロードするのかといった、コンテンツの扱い方に関する情報を伝えるヘッダー。
代表的な指定は次の2つ。
Content-Disposition: inlineContent-Disposition: attachmentinline はブラウザ内で表示可能なコンテンツを通常どおり扱う方向の指定。
attachment はダウンロードする添付ファイルとして扱う指定。ブラウザによっては、filename が指定されていれば、その名前を保存候補として利用する。
Content-Typeとの違い
今回、最初に気になったのがここ。
たとえばJPEG画像を返す場合、
Content-Type: image/jpegとする。
これは、
「このデータはJPEG画像です」
という情報。
一方、
Content-Disposition: attachmentは、
「このレスポンスは添付ファイルとして扱います」
という情報。
つまり、
Content-Type
↓
「これは何のデータ?」
Content-Disposition
↓
「そのデータをどう扱う?」という違いとして考えると分かりやすい。
Content-Type はメディアタイプを示し、Content-Disposition はコンテンツの扱い方を示す。
なぜContent-Dispositionが必要になったのか?
Content-Typeだけでは「どう扱うか」までは決まらない(Content-Typeだけではダメなの?)
たとえば、
Content-Type: image/jpegと返せば、JPEGであることは伝えられる。
ただし、
「そのJPEGをブラウザに表示するのか、保存させるのか」
という情報は別。
そこで、
Content-Disposition: attachmentのような指定が使われる。
今回の検証では、この違いを実際に確認してみる。
Content-Dispositionにはどのような指定がある?
今回の記事では、主に次の4パターンを確認する。
| 指定 | 今回確認する内容 |
|---|---|
| なし | ヘッダーを指定しないとどうなるか |
inline | 通常の表示として扱われるか |
attachment | ダウンロードとして扱われるか |
attachment; filename=... | 保存時のファイル名が変わるか |
さらに、ファイル名について、
filename="sample.txt"と、
filename*=UTF-8''...の違いも確認する。
実際にContent-Dispositionを試してみる
検証環境
| 項目 | 内容 |
|---|---|
| OS | Windows 11 |
| 言語 | JavaScript |
| ランタイム | Node.js v24.14.1 |
| HTTPサーバー | Node.js標準 http |
| ブラウザ | Google Chrome |
| 検証ファイル | TXT / JPEG |
Node.jsの response.setHeader() を使うことで、HTTPレスポンスのヘッダーを設定できる。Node.jsでは、ヘッダー値に不正な文字が含まれている場合、TypeError が発生する。また、UTF-8文字をヘッダー値として扱う場合にはRFC 8187形式を使用する例がNode.jsのドキュメントにも掲載されている。
今回確認したこと
今回の検証では、以下を確認する。
Content-Dispositionがない場合、Chromeはどう扱うのかinlineとattachmentでは何が変わるのかfilenameを指定すると保存ファイル名が変わるのか- スペースを含むファイル名はどう扱われるのか
- 日本語を
filenameに直接指定するとどうなるのか filename*を使うとどうなるのかfilenameとfilename*を両方指定するとどちらが使われるのか- JPEGでも同じような挙動になるのか
検証方法
今回の検証では、ChromeのDevToolsを利用する。
基本的に確認するのは次の3つ。
1. Network
HTTPレスポンスに実際にどのヘッダーが返ってきたか確認する。
特に、
Response Headersを確認する。
2. Chromeの画面
レスポンスをChromeが実際にどう処理したのかを確認する。
たとえば、
- ブラウザ内に表示された
- ダウンロードが開始された
など。
3. 保存されたファイル
ダウンロードが発生した場合は、実際に保存されたファイル名も確認する。
検証1:Content-Dispositionを指定しない
まずは Content-Disposition をいっさい指定しないケース。
今回使用するTXTでは、
Content-Type: text/plainだけを返す。
検証コード
case '/case01-no-disposition-txt':
sendText(res);
break;sendText() の中では Content-Type だけを設定している。
function sendText(res, contentDisposition) {
res.setHeader('Content-Type', 'text/plain');
if (contentDisposition !== undefined) {
res.setHeader('Content-Disposition', contentDisposition);
}
res.end(txtData);
}検証結果
Response HeadersにContent-Dispositionがないことも確認。
↓
Chromeの画面にテキストが表示された。
検証結果から分かったこと
Chromeでは単なるテキストとしてブラウザ表示されたっぽい。
検証2:inline
次に、
Content-Disposition: inlineを指定してみる。
検証コード
case '/case02-inline-txt':
sendText(res, 'inline');
break;どうなる?
仕様上、inline はコンテンツを通常の表示方法で扱う指定。
ただし、今回は「仕様上そうなっている」だけで終わらせず、Chromeで実際に確認してみる。
検証結果
Response Headersで以下になっていることを確認。
↓
Content-Disposition: inlineが付与されているが、Chromeの画面にテキストが表示された。
検証結果から分かったこと
inline を指定してもブラウザ内に表示されるっぽい。
検証3:attachment
次に、
Content-Disposition: attachmentを指定してみる。
検証コード
case '/case03-attachment-txt':
sendText(res, 'attachment');
break;attachment はコンテンツをダウンロードする添付ファイルとして扱うための指定。
検証結果
Response Headersで以下になっていることを確認。
Content-Disposition: attachment
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:case03-attachment-txt.txt
検証4:filenameを指定する
ここからファイル名指定を確認してみる。
Content-Disposition: attachment; filename="sample.txt"検証コード
case '/case04-filename-txt':
sendText(
res,
'attachment; filename="sample.txt"'
);
break;filename は、保存時などに利用するファイル名を示すためのパラメータ。
検証結果
Response Headersで以下になっていることを確認。
Content-Disposition: attachment; filename="sample.txt"
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:sample.txt
検証結果から分かったこと
filename に指定した名前で、Chromeでも保存された。
検証5:スペースを含むfilename
次に、
Content-Disposition: attachment; filename="sample file.txt"を試してみる。
ファイル名にスペースなどを含める場合、filename の値を引用符で囲む形式が利用できる。
検証コード
case '/case05-filename-space-txt':
sendText(
res,
'attachment; filename="sample file.txt"'
);
break;検証結果
Response Headersで以下になっていることを確認。
Content-Disposition: attachment; filename="sample file.txt"
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:sample file.txt
検証結果から分かったこと
filename にスペースを含む場合は、引用符で囲むことでChromeでも正しくファイル名が保存されるっぽい。
検証6:filenameに日本語を直接指定する
ここで、次のような日本語ファイル名を指定してみる。
Content-Disposition: attachment; filename="日本語.txt"検証コード
case '/case06-filename-japanese-txt':
sendText(
res,
'attachment; filename="日本語.txt"'
);
break;この検証は、ChromeだけでなくNode.js側も重要。
Node.jsのHTTP APIでは、ヘッダー値に不正な文字が含まれている場合 TypeError になる。Node.jsのドキュメントでは、UTF-8文字を含む値を扱う場合にRFC 8187形式を使用する例も示されている。
Node.js側のターミナルでエラーが発生した。
TypeError
ERR_INVALID_CHAR
Chrome側はエラーレスポンスが返ってきている。
検証結果
Node.js側でエラーが発生したため、Chrome側でレスポンスを受け取れず500エラーになった。
エラーとしては TypeError の ERR_INVALID_CHAR が発生している。
検証7:filename*を使う
ここで filename* を試してみる。
たとえば、
Content-Disposition: attachment; filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txtとする。
filename* は拡張パラメータ形式を使用し、文字コードを指定した値を表現できる。RFC 8187では、UTF-8''... のような形式で文字コードと値を指定する。
検証コード
case '/case07-filename-star-txt':
sendText(
res,
"attachment; filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txt"
);
break;検証結果
Response Headersで以下になっていることを確認。
Content-Disposition: attachment; filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txt
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:日本語.txt
検証結果から分かったこと
filename* を使うことで、Chromeでも日本語ファイル名が正しく保存されるようになった。
検証8:filenameとfilename*を両方指定する
次に、2つを同時に指定してみる。
Content-Disposition: attachment;
filename="fallback.txt";
filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txtRFC 6266では、両方が存在する場合、filename* を理解するユーザーエージェントは filename* を選択することが推奨されている。古いユーザーエージェントへのフォールバックとして filename と filename* の両方を指定する形も示されている。
検証コード
case '/case08-filename-both-txt':
sendText(
res,
"attachment; filename=\"fallback.txt\"; filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txt"
);
break;検証結果
Response Headersで以下になっていることを確認。
Content-Disposition: attachment; filename="fallback.txt"; filename*=UTF-8''%E6%97%A5%E6%9C%AC%E8%AA%9E.txt
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:日本語.txt
仕様との比較
仕様上の説明
↓
filename* が優先される
実際のChrome
↓
filename* の指定 (日本語.txt) で保存された
比較
↓
一致したfilename としては "fallback.txt" にしていたが、filename* に日本語を指定したため、日本語.txt の名前で保存された。
検証9:inline + filenameをJPEGで確認する
ここからJPEGを使ってみる。
レスポンスを次のように設定。
Content-Type: image/jpeg
Content-Disposition: inline; filename="sample.jpg"検証コード
case '/case09-inline-filename-jpg':
sendJpeg(
res,
'inline; filename="sample.jpg"'
);
break;ここでは、
filenameを指定していても、inlineの場合はどうなるのか?
を確認する。
検証結果
Response Headersで以下になっていることを確認。
Content-Type: image/jpeg
Content-Disposition: inline; filename="sample.jpg"
↓
保存されたファイル:なし(ダウンロードされず、ブラウザ上で表示された)
検証結果から分かったこと
inline はダウンロードダイアログが表示されず、ブラウザ画面で表示されるっぽい。
検証10:JPEG + attachment
最後に、同じJPEGを attachment にしてみる。
Content-Type: image/jpeg
Content-Disposition: attachment; filename="sample.jpg"検証コード
case '/case10-attachment-jpg':
sendJpeg(
res,
'attachment; filename="sample.jpg"'
);
break;検証結果
Response Headersで以下になっていることを確認。
Content-Type: image/jpeg
Content-Disposition: attachment; filename="sample.jpg"
↓
ダウンロードダイアログが表示されることを確認。
↓
保存されたファイル名:sample.jpg
検証結果をまとめる
10個の検証を実施したら、最後に表で比較する。
| No. | ファイル | Content-Disposition | Chromeの結果 | 保存ファイル名 |
|---|---|---|---|---|
| 1 | TXT | なし | 画面表示 | - |
| 2 | TXT | inline | 画面表示 | - |
| 3 | TXT | attachment | ダウンロード | case03-attachment-txt.txt |
| 4 | TXT | attachment; filename="sample.txt" | ダウンロード | sample.txt |
| 5 | TXT | attachment; filename="sample file.txt" | ダウンロード | sample file.txt |
| 6 | TXT | attachment; filename="日本語.txt" | エラー (500) | - |
| 7 | TXT | attachment; filename* | ダウンロード | 日本語.txt |
| 8 | TXT | attachment; filename + filename* | ダウンロード | 日本語.txt |
| 9 | JPEG | inline; filename="sample.jpg" | 画面表示 | - |
| 10 | JPEG | attachment; filename="sample.jpg" | ダウンロード | sample.jpg |
実際に調べて分かったこと
調べる前
最初は、
Content-Dispositionは、ファイルをダウンロードさせるために必要なHTTPヘッダー
くらいの認識だった。
調べた後
調べてみると、Content-Disposition は「ファイルをダウンロードするためだけ」のものではなく、レスポンスを通常どおり扱うのか、添付ファイルとして扱うのかを示すための仕組みだと分かった。
また、filename や filename* を利用することで、保存時などに利用するファイル名についても伝えられる。
一方、Content-Type はデータのメディアタイプを示すためのもの。
そのため、
「
Content-Typeがあるのに、なぜContent-Dispositionも必要なのか?」
という最初の疑問については、
そもそも役割が違うから
という理解になった。
注意点
今回の検証は、以下の環境で実施している。
- Windows 11
- Node.js v24.14.1
- Google Chrome
- Node.js標準のHTTPサーバー
そのため、今回のChromeで確認した結果を、そのまま「すべてのブラウザで同じ」と考えることはできない。
特にファイル名については、ブラウザや保存先のファイルシステムによって調整される場合がある。MDNでも、ブラウザがファイルシステム上の制約に合わせてファイル名を変換する場合があると説明されている。
また、Content-Disposition がなくてもブラウザがリソースを取得したり保存したりする場合がある。
今回調べて分からなかったこと
今回の記事ではChromeに絞って検証している。
そのため、次のような内容は今回の調査範囲外。
- FirefoxやSafariでの挙動
<a download>を利用したダウンロードとの関係- 複数ブラウザ間での
filename/filename*の比較 - Windows以外のファイルシステムでのファイル名処理
- 長すぎるファイル名の詳細な挙動
また時間あるときにでも、調査してみようかな、、、
まとめ
今回は、ファイルダウンロードを自作しているときに気になった Content-Disposition について調べた。
最初は、
「ファイルをダウンロードさせるために必要なヘッダー」
という認識だった。
しかし、調査と検証を通して、より正確には、
Content-Typeはデータの種類を示すContent-Dispositionはコンテンツの扱い方を示すinlineは通常の表示として扱う指定attachmentは添付ファイルとして扱う指定filenameで保存時などに利用するファイル名を示せるfilename*で文字コードを指定できる形式でファイル名を示せるfilenameとfilename*を両方指定する方法もある
ということが分かった。
最初に疑問だった、
「
Content-Typeがあるのに、なぜContent-Dispositionも必要なの?」
については、
Content-TypeとContent-Dispositionは、そもそも伝えている情報が違うから
という結論になった。
また、
「
Content-Dispositionがないとファイルをダウンロードできないの?」
については、単純に「必須」と考えるのではなく、実際のブラウザがレスポンスをどのように扱うかを確認する必要があるということも分かった。
参考資料
今回使用したGithubコード群
- RFC 6266 - Use of the Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)
- RFC 8187 - Indicating Character Encoding and Language for HTTP Header Field Parameters
- MDN - Content-Disposition header
- Node.js v24 HTTP API Documentation