by WebSurfer
2013年2月16日 16:50
画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。
.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。
MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。
このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。
そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。
従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。
以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。
-
Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
-
Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
-
Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
-
html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
-
Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。
.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。
http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。
(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)
<%@ WebHandler Language="C#" Class="Handler" %>
using System;
using System.Web;
public class Handler : IHttpHandler
{
public void ProcessRequest (HttpContext context)
{
HttpResponse response = context.Response;
HttpRequest request = context.Request;
string fileName = "日本語.txt";
// IE の場合、日本語ファイル名の文字化け対策が必要。
// Firefox, Chrome, Safari, Opera の場合は不用。
// 2014/3/3 修正
// IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
// または "InternetExplorer" (.NET 4) になる。IE の場合
// User Agent には必ず "Trident" と言う文字列が入ってい
// るらしいので、そちらで判定した方がよさそう。
if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
|| request.UserAgent.Contains("Trident"))
{
fileName = context.Server.UrlEncode(fileName);
}
// キャッシュを許可するか否か、許可する場合は有効期限を
// 指定しておくべき。
// 以下のコードはキャッシュを許可しない場合の例。応答ヘ
// ッダーは次のようになる。
// Cache-Control: no-cache
// Pragma: no-cache
// Expires: -1
response.Cache.SetCacheability(HttpCacheability.NoCache);
response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));
// ブラウザによってファイルの種類を判断する方法が異なる。
// IE は、Content-Disposition: ヘッダが存在する場合は、
// filename パラメータで設定されたファイル名の拡張子を
// 優先的に使う。Content-Type: ヘッダの指定は無視される。
// 逆に、Opera など、Content-Type: ヘッダの指定を優先的
// に使うものもある。なので、Content-Disposition: ヘッダ
// と Content-Type: ヘッダの両方を正しく指定しておいた方
// がよさそう。
response.AppendHeader("Content-Disposition",
"attachment;filename=" + fileName);
response.ContentType = "text/plain";
// 文字列 "Hello World" を応答 HTTP ストリームに書込み。
// ファイルの場合は TransmitFile メソッドを使うのがお勧
// め(WriteFile メソッドは大きなファイルは扱えないので)
response.Write("Hello World");
// Flush, End, Close メソッド等は使用しないこと。
}
public bool IsReusable
{
get
{
return false;
}
}
}
呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。