WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ダウンロードは HTTP ハンドラで

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 ページを利用する限り決定打はなさそうです。

  1. Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
  2. Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただし、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
  3. Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
  4. html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
  5. 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 を設定するのがよさそうです。

Tags: ,

Upload Download

ダウンロードファイル名の文字化け

by WebSurfer 2011年3月20日 12:33

2014/3/2 改版
IE9 より RFC 6266 (RFC 2231/RFC 5987) がサポートされたということで、その点を考慮してこの記事を全面書き換えました。

ASP.NET Web アプリでファイルをダウンロードする際、AppendHeader メソッドを使って Content-Disposition ヘッダフィールドの attachment; filename= に日本語のファイル名を設定し、受信側のブラウザに IE を使用すると、ファイル名が以下の画像に示すように文字化けします。(以下の画像の例では、オリジナルのファイル名を "日本語のファイル名.xls" としています)

ダウンロードファイル名の文字化け

その理由は MSDN Blog の記事 Downloads and International Filenames をそのまま引用すると "Early versions of Internet Explorer worked around this limitation by assuming that any non-ASCII characters within HTTP headers were encoded using the local system's Windows codepage" ということだそうです。

上記の英文を具体的に説明すると次の通りです。

例えば以下のコードのよう AppendHeader メソッドを使ってファイル名を設定すると、応答ヘッダで "日本語" の部分は UTF-8 のバイト列 e6 97 a5 e6 9c ac e8 aa 9e となります。(Fiddler2 の HexView で見ると分かります。ASP.NET Web アプリではデフォルトでは応答ヘッダはすべて UTF-8 になりますので、ファイル名の部分も UTF-8 のバイト列として送信されます)

Response.AppendHeader("Content-Disposition",
      "attachment;filename=日本語.xls");

上の MSDN Blog に書いてあるように、応答ヘッダに ASCII コード以外の文字(上の例では e6 97 a5 e6 9c ac e8 aa 9e)がある場合、その部分は自分の PC の Windows codepage で、即ち日本語 OS の場合は Shift_JIS として解釈されます。

IE は e6 97 a5 e6 9c ac e8 aa 9e を Shift_JIS コードとして解釈しようとするので、正しくデコードできず文字化けするというわけです。(ちなみに、Shift_JIS では "日本語" は 93 fa 96 7b 8c ea になります)

解決策は、マイクロソフト サポートオンラインの記事 ファイルをダウンロードする ASP.NET ページで日本語ファイル名が文字化けする に以下の 3 つの方法が紹介されています。

  1. HttpServerUtility.UrlEncode メソッドを使ってファイル名をエンコードする。
  2. HttpResponse.HeaderEncoding プロパティを使って Shift_JIS のヘッダを送信する。
  3. RFC 6266 に準拠して attachment; filename*= UTF-8''%e2%82%ac%20rates のようにする。

1 番目の方法は、一部のブラウザが対応していないという点が問題です。IE, Firefox, Chrome, Safari, Opera で検証した結果は以下の表の通りでした。

ブラウザ UrlEncode
あり なし
IE OK NG
文字化けする
Firefox NG
エンコードされた文字になる
OK
Chrome OK OK
Safari NG
エンコードされた文字になる
OK
Opera OK OK

上記の結果は、最初にこの記事を書いた 2011 年 3 月 20 日の時点で検証に使ったブラウザ IE8, Firefox 3.6.15, Chrome 10.0.648.151, Safari 5.0.4, Opera 11.01 でも、この記事を改版した 2014 年 3 月 2 日の時点で検証に使ったブラウザ IE9, Firefox 27.0.1, Chrome 33.0.1750.117, Safari 5.1.7, Opera 12.16 でも同じでした。

Firefox と Safari の場合は、UrlEncode するのはダメで、%e6%97%a5%e6%9c%ac%e8%aa%9e.xls のようなエンコードされた名前になってしまいます。Chrome と Opera は UrlEncode 有り/無しどちらも OK でした。

また、IE の場合でも、ファイル名に半角空白が含まれているとうまく行きません。UrlEncode メソッドは、半角空白を + に変換するので、例えば "file name.txt"(file と name の間に半角空白)" は "file+name.txt" になってしまいます。なので、半角空白は URL エンコードせず、そのままにしておく必要があります。

半角空白を + ではなく %20 に変換(パーセントエンコーディング)しても問題があります。IE 側で開く「ファイルのダウンロード」ダイアログで[開く(O)]をクリックするとメモ帳が開いて中身が表示されますが(もちろん拡張子 txt がメモ帳に関連付けられているとして)、メモ帳のウィンドウの左上に表示されるファイル名の空白は %20 のままになります。(上記の例では "file%20name.txt" となる) まぁ、これは大した問題ではないのかもしれませんが、気分がよくないです。(笑)

2 番目の方法(Shift_JIS のヘッダを送信)は IE ではうまく行きますが、当然、IE 以外のブラウザは対応できません。

また、ファイル名を Shift_JIS にすると、IE 自体は対応できても、それ以前にプロキシサーバーで文字化けして、結局、文字化けは回避できないという可能性があります。それに、単にファイル名が文字化けするだけでなく、他に予期しない好ましからざる副作用が出る恐れもあります。従って、2 番目の方法は、使用するブラウザが IE に限定される場合でも避けた方が賢明だと思います。

3 番目の方法は RFC 6266 をサポートしていない IE8 以下には使えないという問題があります。Safari 5.1.7 も RFC 6266 をサポートしていないようです。また、これも、UrlEncode メソッドで、半角空白が + に変換されることにより、"file name.txt"(file と name の間に半角空白)" が "file+name.txt" になってしまうという問題があります。半角空白はパーセントエンコーディング("+" ではなくて "%20" に変換)しないとうまく行きません。

結局、現状ではファイル名には ASCII 文字以外は使わないというのが一番の解決策だと思います。

どうしても日本語のファイル名を使って、かつ IE, Firefox, Chrome 等の複数のブラウザに対応するなら、以下のようにブラウザによって応答ヘッダを切り替える他方法はなさそうです。

(注)別の記事 ダウンロードは HTTP ハンドラで で書きましたように、ファイルなどをダウンロードするのに .aspx ページを利用するといろいろ問題がありそうなので、以下のサンプルコードでは HTTP ハンドラを使用しています。

<%@ WebHandler Language="C#" Class="DownloadHandler" %>

using System;
using System.Web;

public class DownloadHandler : IHttpHandler 
{    
  public void ProcessRequest (HttpContext context) 
  {
    HttpResponse response = context.Response;
    HttpRequest request = context.Request;

    string fileName = "日 本 語 (+japanese+).txt";

    // + は %2b に変換され、空白は + に変換される。
    string encodedFileName = context.Server.UrlEncode(fileName); 
    encodedFileName = encodedFileName.Replace("+", "%20");
        
    response.Clear();

    response.ContentType = "text/plain";
        
    // キャッシュを許可しない
    response.Cache.SetCacheability(HttpCacheability.NoCache);
    response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
    response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

    HttpBrowserCapabilities browser = request.Browser;

    // IE の場合。
    // IE11 では Browser プロパティは "Mozilla" (.NET 2.0) 
    // または "InternetExplorer" (.NET 4) になる。IE の場合
    // User Agent には必ず "Trident" と言う文字列が入ってい
    // るらしいので、そちらで判定した方がよさそう。
    if (browser.Browser.ToUpper().IndexOf("IE") >= 0 ||
        request.UserAgent.Contains("Trident"))            
    {
      // IE8 以下(RFC 6266 未サポート)
      // 空白を %20 のままにしておくと処理がうまくいかない。
      // 「ファイルのダウンロード」ダイアログで[開く(O)]を
      // クリックするとメモ帳が開いて中身が表示されるが %20 
      // がそのままウィンドウの左上に表示されるファイル名に
      // 含まれる。それが気になる場合は、以下のように %20 を
      // 空白に置き換える。(注:「ファイルのダウンロード」
      // ダイアログで[保存(S)]をクリックすれば何故か %20 
      // は空白に変換されて保存される)
      if (browser.MajorVersion < 9)
      {
        response.AppendHeader("Content-Disposition", 
            "attachment;filename=\"" + 
            encodedFileName.Replace("%20", " ") +  "\"");
      }
      // IE9 以上(RFC 6266 サポート)
      else
      {
        response.AppendHeader("Content-Disposition", 
            "attachment;filename*=utf-8''" + encodedFileName);
      }
    }
    // IE 以外
    else
    {
      // Safari 5.1.7 がまだ RFC 6266 未対応らしいので 
      // filename="xxxxx" の併記が必要。 
      response.AppendHeader("Content-Disposition", 
          "attachment;filename=\"" + fileName + 
          "\";filename*=utf-8''" + encodedFileName);
    }

    response.Write("こんにちは世界!");
  }
 
  public bool IsReusable 
  {
    get 
    {
      return false;
    }
  }
}

なお、IIS7 で試した限りですが(他のサーバーは不明)、以下のコードのように HyperLink で直リンクすれば、どのブラウザでも文字化けはありませんでした。

<asp:HyperLink 
    ID="HyperLink2" 
    runat="server" 
    NavigateUrl="~/日 本 語 (japanese).zip">
    日 本 語 (japanese).zip
</asp:HyperLink>

その理由は、ブラウザから GET 要求する url のファイル名はパーセントエンコーディング(半角空白は "+" ではなく "%20")され、応答ヘッダには以下のように正しく Content-Type が指定されると共に Content-Disposition フィールドが含まれないからです。

HTTP/1.1 200 OK
Content-Type: application/x-zip-compressed
Last-Modified: Fri, 21 Feb 2014 14:39:36 GMT
Accept-Ranges: bytes
ETag: "752e46be122fcf1:0"
Server: Microsoft-IIS/7.0
X-Powered-By: ASP.NET
Date: Sun, 02 Mar 2014 07:42:01 GMT
Content-Length: 1956

なお、直リンクする場合、NavigateUrl に指定するファイル名に "+" を入れると、IIS7 では HTTP エラー 404.11(ダブルエスケープシーケンスを含む要求の拒否)になるので注意してください。

Tags:

Upload Download

ダウンロードの際の拡張子 と MIME Type の指定

by WebSurfer 2010年9月2日 22:42

MSDN フォーラムで、IE で xlsm をダウンロードすると zip 形式になってしまい、直接 Excel で開けないという話があったので、ちょっと調べてみました。

結局、単にそれはサーバー側の Web アプリの問題だったのですが、調査の結果、自分が知らなかったことがいろいろあったので、忘れないように書いておきます。

ブラウザは、ヘッダーの Content-Disposition: attachment; filename= で指定する拡張子と、Content-Type: で指定する MIME Type のどちらでファイルの種類を判断するでしょうか?

検証した結果は以下のとおりでした。簡単に言うと、IE8 は拡張子で判断、Opera は MIME Type で判断、Forefox は拡張子と MINE Type の指定が違う場合結果は未定義のようです。

ブラウザ Ext: xlsm
MT: xlsm
Ext: xlsm
MT: zip
Ext: zip
MT: zip
Ext: zip
MT: xlsm
IE8 Excel Excel Zip Zip
Firefox 3.6.8 Excel Zip Zip Zip
Opera 10.61 Excel Zip Zip Excel

上の表で、Ext は Content-Disposition に設定した拡張子、MT は Content-Type に設定した MIME Type です。

検証に使ったファイルは、有効な xlsm 形式のファイルで、拡張子は正しく xlsm としており、検証の間、中身も名前も一切変えていません。

クライアントの OS は Vista SP2 です。拡張子とアプリケーションの関連付けは、xlms は Excel、zip は WinZip としています。Web サーバーは Windows Server 2008 の IIS7, ASP.NET 3.5 SP1 です。

予想外の動きをしたのは Opera で、拡張子と MIME Type が異なる場合、拡張子を MIME Type に合わせて書き換えてしまいます。下の画像は、拡張子を xlsm、MIME Type を zip とした場合の例です。元のファイル名 001.xlsm が 001.zip に書き換えられています。

Opera のファイルダウンロード時のダイアログ

直リンクの場合(a 要素の href 属性にサーバーに置いた xlsm ファイルを指定した場合)、ヘッダーは以下のようになります。ブラウザは正しく判断出来るようです。

Content-Type: application/vnd.ms-excel.sheet.macroEnabled.12
Last-Modified: Thu, 02 Sep 2010 13:07:58 GMT
Accept-Ranges: bytes
ETag: "3e8647dd9f4acb1:0"
X-Powered-By: ASP.NET
Date: Thu, 02 Sep 2010 13:12:12 GMT
Content-Length: 12477

ちなみに、HTTP ハンドラーを使った場合のヘッダーは以下のようになります。

Cache-Control: private
Content-Type: application/vnd.ms-excel.sheet.macroEnabled.12
Content-Disposition: attachment; filename=001.xlsm
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Date: Thu, 02 Sep 2010 13:21:00 GMT
Content-Length: 12477

検証に使った HTTP ハンドラのコードを参考までにアップしておきます。

<%@ WebHandler Language="C#" Class="_045_Handler" %>

using System;
using System.Web;
using System.IO;

public class _045_Handler : IHttpHandler 
{
  public void ProcessRequest(HttpContext context)
  {
    string path = 
      context.Server.MapPath("~/Test/data/001.xlsm");

    if (!File.Exists(path))
    {
      return;
    }
        
    string type = context.Request.QueryString["type"];

    switch (type)
    {
      case "xlsm_xlsm":
        SetHeader(context, "001.xlsm", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;
      case "xlsm_zip":
        SetHeader(context, "001.xlsm", 
          "application/x-zip-compressed");
        break;
      case "zip_zip":
        SetHeader(context, "001.zip", 
          "application/x-zip-compressed");
        break;
      case "zip_xlsm":
        SetHeader(context, "001.zip", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;
      default:
        SetHeader(context, "001.xlsm", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;                
    }
    context.Response.TransmitFile(path);
    context.Response.End();
  }
    
  public bool IsReusable
  {
    get {
      return false;
    }
  }

  protected void SetHeader(HttpContext context, string name, string type)
  {
    context.Response.AppendHeader("Content-Disposition",
      "attachment; filename=" + name);
    context.Response.ContentType = type;
  }
}

他に、今回知った新(?)事実は、Excel 2007 からファイル形式が変わったということです。何をいまさらと言われるかも知れませんが。(汗)

Excel 2007 からはファイルはバイナリではなくなり、xml 形式のテキストを zip で圧縮したものになっているそうです。確かに WinZip で xlsm ファイルを開くことができ、中身は xml 形式のテキストファイルでした。

また、xlsm は「マクロ有効ブック」というそうで、セキュリティ対策として、拡張子からマクロが含まれていることが分かるような名前の規則を作ったそうです。

------------ 2011/5/21 追記 ------------

IE8 は拡張子で判断と書きましたが、やはり IE はそのような仕様になっているようです。以下のページを参考にしてください。

ファイルのダウンロードダイアログで表示されるファイル名の命名規則

即ち、上のページの「詳細」の 1 番目に書いてあるとおり「Content-Disposition: ヘッダが存在する場合は、filename パラメータで設定されたファイル名が利用されます。」ということです。

サポートページには書いてないですが、Content-Disposition: ヘッダでファイル名が指定してあれば、Content-Type: の指定は無視されます。

2 番目の「HTTP レスポンス内に存在する、送られてきた Content-Type が、レジストリの HKEY_CLASS_ROOT\MIME\Database\Content Type 以下に存在するかどうか・・・」というのは、Content-Disposition: ヘッダの filename パラメータにファイル名の指定がない場合に限るようです。

Tags: ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2017年8月  >>
303112345
6789101112
13141516171819
20212223242526
272829303112
3456789

View posts in large calendar