回應標頭、緩衝、重導向


你可以使用 HttpServletResponse 物件上的 setHeader()addHeader() 來設定回應標頭,setHeader() 設定標頭名稱與值,addHeader() 則可以在同一個標頭名稱上附加值。

setHeader()addHeader() 方法接受字串值,如果標頭的值是整數,則可以使用 setIntHeader()addIntHeader() 方法,如果標頭的值是個日期,則可以使用 setDateHeader()addDateHeader() 方法。

有些標頭必須搭配 HTTP 狀態碼(Status code),設定狀態碼的話,可以透過 HttpServletResponsesetStatus() 方法,例如,正常回應的 HTTP 狀態碼為 200 OK,可以透過 HttpServletResponse.SC_OK 來設定,如果想要重新導向頁面,必須傳送狀態碼 301 Moved Permanently、302 Found,前者可以透過 HttpServletResponse.SC_MOVED_PERMANENTLY 取得,後者可以透過 HttpServletResponse.SC_SC_FOUND(建議)或 HttpServletResponse.SC_MOVED_TEMPORARILY 取得。

例如,若某個資源也許永久性地移動至另一個網址,當客戶端請求原有網址時,必須要求客戶端重新導向至新網址,並要求未來連結時也應使用新網址的話(像是告訴搜尋引擎網站搬家了,這有利於網址 SEO),可以如下撰寫程式:

response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);  
response.addHeader("Location", "new_url");

如果資源只是暫時性搬移,或者是將來可能改變,仍希望客戶端依舊使用現有位址來存取資源,不要快取資源之類的,可以使用暫時重定向:

response.setStatus(HttpServletResponse.SC_FOUND);  
response.addHeader("Location", "temp_url");

所有的標頭設定,必須在回應確認之前(Commit),在回應確認之後設定的標頭,會被容器忽略。

除了 301、302 之外,HTTP 1.1 增加了 303 See Other 與 307 Temporary Redirect 狀態碼,如果不擔心古老只支援 HTTP 1.0 的客戶端問題,詳情可參考〈Status Code Definitions〉。

容器可以(但非必要)對回應進行緩衝,通常容器預設都會對回應進行緩衝,你可以操作 HttpServletResponse 以下有關緩衝的幾個方法:

  • getBufferSize()
  • setBufferSize()
  • isCommitted()
  • reset()
  • resetBuffer()
  • flushBuffer()

setBufferSize() 必須在呼叫 HttpServletResponsegetWriter()getOutputStream() 方法之前呼叫,所取得的 WriterServletOutputStream 都會套用這個設定,在呼叫 HttpServletResponsegetWriter()getOutputStream() 方法之後呼叫 setBufferSize(),會丟出 IllegalStateException

在緩衝區未滿之前,所設定的回應相關內容都不會真正傳至客戶端,你可以使用 isCommitted() 看看是否回應已確認。如果想要重置所有的回應資訊,可以呼叫 reset() 方法,這會連同已設定的標頭一併清除,呼叫 resetBuffer() 會重置回應內容,但不會清除已設定的標頭內容。

flushBuffer() 會出清(flush)所有緩衝區中已設定的回應資訊至客戶端,reset()resetBuffer() 必須在回應未確認前呼叫,在回應已確認後呼叫 reset()resetBuffer() 會丟出 IllegalStateException

對於暫時重定向,除了自行透過 HTTP 狀態碼與 Location 標頭的設定之外,還可以使用 HttpServletResponsesendRedirect() 要求瀏覽器重新請求另一個 URL,例如:

response.sendRedirect("https://openhome.cc");

這個方法會在回應中設定 HTTP 狀態碼 302 以及 Location 標頭,無論是自行控制狀態碼、標頭,或者是透過 sendRedirect() 方法重定向,客戶端會使用 GET 方法請求指定的 URL,因此在網址列上會發現 URL 的變更。由於是利用 HTTP 狀態碼與標頭資訊,要求瀏覽器重新導向網頁,因此這個方法必須在回應未確認輸出前執行,否則會發生 IllegalStateException

重新定向的使用時機之一,是在表單 POST 之後,為了避免使用者在 POST 表單之後,重新載入網頁造成重複發送 POST 內容,可以在 POST 之後要求重新導向,重定向的使用時機之二是使用者登入後自動導回先前閱讀之頁面,例如,若目前頁面為 xyz.html,設定一個鏈結為 login?url=xyz.html,在使用者登入成功之後取得 url 請求參數來進行重新導向。

如果重新導向的目的地,是根據使用者的指定,特別是允許使用者指定外部網址的開放式重新導向,請務必特別小心,以免成為被攻擊的弱點,如果非得開放式重新導向,務必檢查允許的對象網址,非開放式重新導向也得小心檢查,或者是對重新導向的目標予以編碼,使用編碼來替代任意的 URL 指定,再於應用程式中對應至真正的 URL。

如果在處理請求的過程中發現一些錯誤,而你想要傳送伺服器預設的狀態與錯誤訊息,則可以使用 sendError() 方法。例如,如果根據請求參數必須傳回的資源根本不存在,則可以如下送出錯誤訊息:

response.sendError(HttpServletResponse.SC_NOT_FOUND);

SC_NOT_FOUND 會令伺服器回應 404 狀態碼。如果想使用自訂的訊息來取代預設的訊息文字,則可以使用 sendError() 的另一個版本:

response.sendError(HttpServletResponse.SC_NOT_FOUND, "筆記文件");

HttpServlet 的以 doGet() 為例,其預設實作就使用了 sendError() 方法:

protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
{
    String protocol = req.getProtocol();
    String msg = Strings.getString("http.method_get_not_supported");
    if (protocol.endsWith("1.1")) {
        resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    } else {
        resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    }
}

sendError() 方法必須在回應未確認輸出前執行,否則會發生 IllegalStateException

回應物件若被容器關閉,則必須出清所有的回應內容,回應物件被關閉的時機點有:

  • Servlet 的 service() 方法已結束
  • 回應的內容長度超過 HttpServletResponsesetContentLength() 設定的長度
  • 呼叫了 sendRedirect() 方法
  • 呼叫了 sendError() 方法
  • 呼叫了 AsyncContextcomplete() 方法(之後還會介紹)