網友Slash提問:

如何在ASP.NET實現將/boo/a.jpg格式的網址導向ashx進行檔案下載?

一時程式毒癮發作,索性把這個題目當成自己的ASP.NET 4 Routing私房練習題止癢,也看看是否能順便解答Slash的疑問。

ASP.NET從4.0起加入內建Routing功能,相較於3.5 SP1做了不少強化,例如: 增加PageRouteHandler以簡便地完成WebPage導向、提供HttpRequest.RequestContext.RouteData方便路徑參數存取... 等等[參考]。

在這個練習中,我打算分別用WebForm(aspx)及Generic Handler(ashx)各實做一次。首要之務是在Global.asax中註冊路由,其中有個小把戲: 在ASP.NET 4中,可以直接用PageRouteHandler處理WebForm導向,卻沒有為ashx提供相似的機制(依Phil Haack的說法,想到時已來不及加進去了),因此要用Phil提供的因應解法,自行宣告一個HttpHandlerRouteHandler類別解決。我們將images/{imgname}註冊給Download.ashx、imgs/{imgname}註冊給DownloadImage.aspx。

<%@ Application Language="C#" %>
<%@ Import Namespace = "System.Web.Routing" %>
<script runat="server">
    void Application_Start(object sender, EventArgs e) 
    {
        RegisterRoutes(RouteTable.Routes);
    }
    
    public static void RegisterRoutes(RouteCollection routes)
    {
        //分別為ashx及aspx註冊路由, 一個用images/*.*, 一個用imgs/*.*
        //ashx註冊時有個小Trick,見HttpHandlerRouteHandler說明
        routes.Add("Images", new Route("Images/{imgname}", 
            new HttpHandlerRouteHandler<DownloadImage>()));
        routes.MapPageRoute("Imgs", "Imgs/{imgname}",
                "~/DownloadImage.aspx");
    }
 
    //ASP.NET 4不支援直接將IHttpHandler視為RouterHandler(太晚想到)
    //在此引用Phil Haack的解決方案
    //http://haacked.com/archive/2009/11/04/routehandler-for-http-handlers.aspx
    public class HttpHandlerRouteHandler<THandler>
    : IRouteHandler where THandler : IHttpHandler, new()
    {
        public IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            return new THandler();
        }
    }

接著看DownloadImage.ashx。由context.Request.RequestContext.RouteData.Values[“imgname”]取出路徑中的圖檔名,檢查該檔是否存在App_Data下,有則以Response.WriteFile()傳回內容,否則丟出404的HttpException,讓IIS視同網頁不存在處理。

using System;
using System.Web;
using System.IO;
 
public class DownloadImage : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        var routeData = context.Request.RequestContext.RouteData.Values;
        string file = context.Server.MapPath(
                      "~/App_Data/" + routeData["imgname"].ToString());
        if (File.Exists(file))
        {
            var resp = context.Response;
            //註: 理論上要由副檔名決定ContentType,此處省略
            resp.ContentType = "image/gif";
            resp.WriteFile(file);
            resp.End();
        }
        else
        {
            throw new HttpException(404, "HTTP/1.1 404 Not Found");
        }
    }
 
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

再來是DownloadImage.aspx,寫法跟ashx接近,唯一的差別是在WebForm中throw HttpException會被IIS當成程式出錯,觸發HTTP 500程式錯誤例外,無法模擬HTTP 404的狀況,即便狀態碼為404,但在未開啟CustomErrors時,會視同程式錯誤顯示例外所在的原始碼內容,開啟後則出現Runtime Error及web.config customErrors mode設定提示,與熟知的找不到網頁訊息形式有些出入,故改為設定StatusCode跟輸出Not Found訊息代替。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.IO;
 
public partial class DownloadImage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string file = Server.MapPath("~/App_Data/" + 
                        (RouteData.Values["imgname"] ?? string.Empty).ToString());
        if (File.Exists(file))
        {
            //註: 理論上要由副檔名決定ContentType,此處省略
            Response.ContentType = "image/gif";
            Response.WriteFile(file);
            Response.End();
        }
        else
        {
            //在WebForm中無法透過throw HttpException方式導向IIS 404錯誤頁
            //故在此自行模擬傳回HTTP 404結果
            Response.Clear();
            Response.StatusCode = 404;
            Response.Status = "404 Not Found";
            Response.Write("Not Found");
            Response.End();
        }
    }
}

執行結果如下:

  1. ashx正確讀取圖檔的情境
  2. ashx找不到圖檔的情境
  3. aspx正確讀取圖檔的情境
  4. aspx找不到圖檔的情境


Comments

# by 小黑

請問黑大,這手法是否同樣是適用 asp.net mvc 中?

# by Ammon

黑大,目前我還沒有遇過 aspx 拋出 HttpException 404 但 IIS 當成 500 的情況。即便是黃色死亡畫面,Status code 依然是 404。這中間是不是有甚麼誤會?

# by Jeffrey

to Ammon, 謝謝指正,的確狀態碼是404沒錯,原本的寫法有誤,我做了調整希望能更精準些。 ...即便狀態碼為404,但在未開啟CustomErrors時,會視同程式錯誤顯示例外所在的原始碼內容,開啟後則出現Runtime Error及web.config customErrors mode設定提示,與熟知的找不到網頁訊息形式有些出入,故改為設定StatusCode跟輸出Not Found訊息代替。

# by Jeffrey

to 小黑, 在ASP.NET MVC中,可以用HttpNotFoundResult。 http://weblogs.asp.net/gunnarpeipman/archive/2010/07/27/asp-net-mvc-3-using-httpnotfoundresult-action-result.aspx

# by Ammon

黑大,custom error 可以依據Status code 設定不同頁面

# by Edward

黑大您好: 我有一個圖片下載的問題, 我到美國gap集團的網頁看衣服, 發現一個奇怪的問題, 某些衣服點進去看後, 檢視原始碼時, 可以看到衣服圖片的網址, 如下:http://oldnavy.gap.com/browse/product.do?cid=26068&vid=1&pid=251613032 而有些衣服點進去看後, 檢視原始碼時, 會出現img src='/', 找不到圖片網址, 但網頁還是看得到圖片, 這是什麼原因呢?? 我利用HttpWebRequest、HttpWebResponse去下載該網站時,原始碼又和在瀏覽器時也不同, 這又是什麼原因呢?? 不好意思,問題有點長,麻煩您撥空看一下,謝謝。

# by Edward

補上看得到圖片,但是src="/"的網址。 如下: http://oldnavy.gap.com/browse/product.do?cid=26068&vid=1&pid=329791022 請用{<div id="productContentLeft"}尋找,謝謝。

# by Jeffrey

to Edward, 你所說的src="/"應該是指這個: <div id="productContentLeft" class="brand3"> <noscript> <div><img alt='Main product image: Men&#39;s Striped Slub-Knit Tees' id="product_image" src='/' /></div> </noscript> <img>被包在<noscript>中,意思是當瀏覽器不支援JavaScript時才適用,絕大多數瀏覽器因為支援JavaScript,看到的圖片並不是這個<img>,而是 id="product_image_bg">裡的<img id="product_image">,而該<img>的src已被JavaScript動態切換成衣服的圖檔(選顏色後會換圖)。 View Source看到的是一開始下載回來的HTML內容,事後JavaScript會去更動其中的src,要使用IE Dev Tools或Firefox之類的工具去看即時的HTML,才是當下的狀態。

# by Edward

了解了,謝謝大大的指導。

# by Edward

黑大您好,可以另外再向您請教一件事嗎?? 我在網址上打“http://oldnavy.gap.com/browse/productData.do?pid=330654022” 看到的內容,跟用HttpWebRequest、HttpWebResponse去下載的內容會完全不一樣。 如果我曾經在瀏覽器上看過該網頁,就會一樣了,請教您知道原因是什麼嗎??謝謝

# by Jeffrey

to Edward, 沒帶Cookie,會被導向 /browse/productData.do?targetURL=http%3A%2F%2Foldnavy.gap.com%2Fbrowse%2FproductData.do%3Fpid%3D330654022&CookieSet=Set,設完Cookie後才連至真正的網址。我猜HttpWebRequest下載到的內容是一個HTTP/1.1 302 Moved Temporarily轉址回應,你不妨比對看看。

# by Edward

嗯,謝謝黑大, 後來觀察封包後, 發現是cookie在作怪, 已經解決該問題了, 感激不盡

Post a comment