重新導向看似簡單:伺服器以狀態碼和 Location 標頭回應,用戶端就前往該位址。但這三位數字決定了瀏覽器能否快取這次重新導向、POST 是否仍維持 POST,以及搜尋引擎可能會將哪個網址編入索引。

本指南涵蓋 RFC 9110 所定義的 3xx 類別、各狀態碼功能重疊背後的歷史、不使用狀態碼的重新導向,以及如何在常見的伺服器上設定正確的狀態碼。

3xx 類別代表什麼

現行的 HTTP 語意規格 RFC 9110 指出,3xx 狀態碼表示「使用者代理程式需要採取進一步的動作,才能完成要求」。第 15.4 節將這些狀態碼分成四種重新導向:

  • 資源可在 Location 所指定的另一個 URI 取得:301、302、307 和 308。
  • 用戶端可以從資源的多種表示形式(representation)中選擇:300。
  • 由另一個資源為要求提供間接回應:303。
  • 用戶端可以重複使用快取中既有的副本:304。

實際上,只有 301、302、303、307 和 308 會作為自動重新導向。瀏覽器所實作的 Fetch Standard,正好把這五個狀態碼稱為「重新導向狀態」(redirect status)。300 通常會像一般頁面一樣顯示,304 則是告訴用戶端使用快取中的副本。

Location 的值可以是相對參照,例如 /new-page 或 ../shop/。用戶端會以剛剛要求的 URL 為基準來解析它(RFC 9110 第 10.2.2 節)。如果 Location 沒有片段識別碼(fragment),則會保留原本的片段:指向 /guide#install 的連結若重新導向至 /docs/guide,最後會抵達 /docs/guide#install。

對這五種真正的重新導向來說,要選用哪個狀態碼,取決於兩個問題:

  1. 搬移是永久的還是暫時的? 永久重新導向可能會被快取,也會向搜尋引擎發出訊號,改將新網址編入索引。
  2. 是否必須保留要求方法? 當原始要求是 POST、PUT 或 DELETE,而不是單純的 GET 時,這一點就很重要。

重新導向狀態碼速覽

狀態碼 名稱 永久 保留方法 預設可快取 常見用途 Google 搜尋
300 Multiple Choices – – 是 提供其他表示形式(罕見) –
301 Moved Permanently 是 否:POST 可能變成 GET 是 網頁與網站搬移、HTTPS 與主機標準化 永久(強烈訊號)
302 Found 否 否:POST 可能變成 GET 否 短期重新導向、登入牆、A/B 測試 暫時(微弱訊號)
303 See Other 否 否:改為 GET 否 表單送出後的結果頁面 暫時
304 Not Modified 不是重新導向 – – 回應條件式要求 內容未變更
305 Use Proxy 已淘汰 – – 請勿使用 –
306 (Unused) 保留 – – 請勿使用 –
307 Temporary Redirect 否 是 否 任何方法的暫時重新導向 暫時
308 Permanent Redirect 是 是 是 API、表單或任何網址的永久搬移 永久(強烈訊號)

「預設可快取」是指該狀態碼依 RFC 9110 第 15.1 節屬於可依經驗法則快取(heuristically cacheable):即使回應沒有明確的新鮮度資訊,快取也可以不向伺服器確認,就直接重複使用該回應。不過,任何回應仍可透過 Cache-Control 設為可快取或不可快取。「Google 搜尋」欄彙整了 Google 有關重新導向與 HTTP 狀態碼的說明文件。

逐一說明各狀態碼

300 Multiple Choices

該資源有多種表示形式,例如不同的語言或檔案格式,可由用戶端自行選擇。回應主體通常會列出各個選項,但沒有標準格式。雖然可以在 Location 中指定偏好的選項,卻無法指望瀏覽器一定會前往該位址。實務上幾乎不會使用 300:伺服器端的內容協商(content negotiation),或是一個附有連結的一般頁面,效果都會更好。

301 Moved Permanently

目標資源「已被指派新的永久 URI,日後對此資源的任何參照,都應使用回應中所附的其中一個 URI」。這是網頁搬移、更換網域、從 HTTP 升級為 HTTPS,以及標準主機重新導向時使用的標準狀態碼。

有兩個細節值得注意。首先,301 可依經驗法則快取,因此瀏覽器可能會記住它,不再詢問伺服器(請參閱下方的快取章節)。其次,基於歷史因素,用戶端在追蹤 301 時可以將 POST 改為 GET,而瀏覽器也確實會這麼做。對於以 GET 載入的頁面,這一點無關緊要;但如果是表單端點或 API,請使用 308。

302 Found

目標資源「暫時位於另一個 URI」,因此用戶端日後的要求仍應使用原本的網址。302 預設不可快取。請將它用於短期情況,例如用特價頁面取代某個分類頁一週、登入牆、依地理位置分流,或 A/B 測試。

和 301 一樣,302 也允許將 POST 改為 GET。許多框架在未指定狀態碼時會送出 302,這也是永久搬移常常不小心變成 302 的原因。

303 See Other

303 會將用戶端重新導向至另一個資源,作為原始要求的間接回應,而用戶端會以 GET(或 HEAD)取得該資源。它是為 Post/Redirect/Get 模式所設計的:伺服器處理完送出的表單後,會回應 303,並附上確認頁或結果頁的網址。之後重新載入該頁面時,只會重複一次無害的 GET,而不會重新送出表單。

303 預設不可快取,也不表示原本的網址已經搬移。Google 會將它視同 302 處理。

304 Not Modified(不是重新導向)

雖然編號屬於 3xx,304 卻從不會把用戶端送往任何地方。它是對條件式 GET 或 HEAD 要求的回應,也就是帶有 If-None-Match 與 ETag 值,或帶有 If-Modified-Since 與日期的要求。如果資源沒有變更,伺服器會回應不含主體的 304,用戶端則使用快取中的副本。

HTTP
GET /styles.css HTTP/1.1
Host: example.com
If-None-Match: "5f1c-3a2b"

HTTP/1.1 304 Not Modified
ETag: "5f1c-3a2b"
Cache-Control: max-age=3600

重新導向檢查工具送出的是非條件式要求,所以通常不會看到 304。若要在 TrueURL 中測試,請先執行一般檢查,從回應標頭中複製 ETag 值,再到自訂要求標頭中,將它以 If-None-Match 標頭送出。對 Googlebot 來說,304 表示內容自上次檢索以來沒有變更。

305 Use Proxy 與 306 (Unused)

305 會要求用戶端透過 Location 中指定的代理伺服器重新送出要求。讓回應能夠變更用戶端的代理伺服器設定有安全風險,因此 RFC 7231 已將這個狀態碼淘汰,目前的瀏覽器也不會依它行動。

306 曾在舊版規格中定義(常被列為 Switch Proxy),現在則保留不用。現代應用程式不應使用這兩個狀態碼;如果看到其中之一,就表示某處設定有誤。

307 Temporary Redirect

307 的意義與 302 相同,但多了一項規定:用戶端不得變更要求方法。POST 會以 POST 的形式,連同相同的主體重新送往新網址。因此,307 是 API 和表單端點適用的暫時重新導向。

您也可能在 Chrome DevTools 中看到從 http:// 到 https:// 的 307 Internal Redirect。這並不是伺服器送出的,而是瀏覽器自行升級了要求,通常是因為它已儲存該網站的 HSTS(Strict-Transport-Security)政策。

308 Permanent Redirect

308 是會保留方法的 301:它是永久重新導向、可依經驗法則快取,而且用戶端不得變更方法。它最早於 2014 年發表在實驗性的 RFC 7238 中,2015 年隨 RFC 7538 成為標準,現在則已納入 RFC 9110。

如果端點會接收 POST、PUT 或 DELETE 要求,永久搬移時請使用 308。對一般頁面來說,301 和 308 可以互換,Google 也會將兩者都視為永久重新導向。

為什麼會有 303、307 和 308

301/302 與 303/307/308 之間的功能重疊,是歷史意外造成的。

HTTP/1.0(RFC 1945,1996 年)定義了 301 Moved Permanently 和 302 Moved Temporarily。它規定使用者代理程式未經使用者確認,不得自動重新導向 POST 要求,而且當時就已指出,有些使用者代理程式會錯誤地將 POST 改成 GET。後來瀏覽器也一直沿用這種做法。

HTTP/1.1 新增了兩個沒有解讀空間的狀態碼。RFC 2068(1997 年)引進了一律改用 GET 的 303 See Other。RFC 2616(1999 年)則新增了一律保留方法的 307 Temporary Redirect,將 302 更名為 Found,並坦白指出大多數使用者代理程式都把 302 當成 303 處理。

但這樣仍然缺少一個會保留方法的永久重新導向,這個缺口直到十五年後才由 308 補上。在此期間,規格也接受了現實:自 RFC 7231(2014 年)起,使用者代理程式在遇到 301 或 302 之後,可以將 POST 改為 GET。

目前,瀏覽器會套用 Fetch Standard 中的下列規則:

狀態碼 原始方法 瀏覽器要求新網址時使用
301、302 POST GET,不含主體
301、302 其他任何方法 相同的方法
303 GET 和 HEAD 以外的任何方法 GET,不含主體
303 GET 或 HEAD 相同的方法
307、308 任何方法 相同的方法與主體

在 301 或 302 之後,只有 POST 會被改寫;PUT 或 DELETE 會保留原本的方法。不過,各家 HTTP 程式庫的行為不一,依賴這一點並不可靠,所以只要要求方法很重要,就請使用 307 或 308。

不使用 3xx 狀態碼的重新導向

meta refresh

HTML
<meta http-equiv="refresh" content="0; url=https://example.com/new-page">

頁面本身以 200 OK 送達,瀏覽器載入頁面之後,才會依照這個標記進行重新導向。其中的數字是延遲秒數。Google 會將立即執行的 meta refresh(HTML 自動重新整理)視為永久重新導向,有延遲的則視為暫時重新導向。

定時重新整理也不利於無障礙使用。WCAG 接受以立即執行的 meta refresh 作為用戶端重新導向(技術 H76),但將定時重新整理列為失敗案例(F40),因為頁面會依照使用者無法控制的計時器自動變更。

JavaScript 重新導向

HTML
<script>
  window.location.replace('https://example.com/new-page');
</script>

location.replace() 不會把執行重新導向的頁面留在瀏覽記錄中,因此使用者按下「上一頁」時,不會又被彈回這次重新導向。指定 location.href 則會將該頁面留在記錄中。

Google 只在轉譯網頁時才會執行 JavaScript,而轉譯是在檢索之後才進行,也有可能失敗。Google 的說明文件建議,只有在無法使用伺服器端重新導向和 meta refresh 重新導向時,才使用 JavaScript 重新導向。許多其他機器人和工具則完全不會執行 JavaScript。

Refresh 標頭

HTTP
HTTP/1.1 200 OK
Content-Type: text/html
Refresh: 0; url=https://example.com/new-page

Refresh 標頭並未在任何 HTTP RFC 中定義。瀏覽器支援它已有數十年,HTML Standard 也將它視為等同於 meta refresh 標記。不過許多 HTTP 用戶端和工具(包括 curl)都會忽略它,因此請優先使用真正的 3xx 重新導向。

如何正確設定重新導向

nginx

nginx
# 兩個主機的所有 http:// 要求,都只經過一個躍點就導向標準 HTTPS 網址
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# https://www.example.com -> https://example.com
server {
    listen 443 ssl;
    server_name www.example.com;
    ssl_certificate     /etc/ssl/example.com.crt;
    ssl_certificate_key /etc/ssl/example.com.key;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    server_name example.com;
    # ssl_certificate 與 ssl_certificate_key 同上

    # 單一搬移的網頁
    location = /old-page {
        return 301 /new-page;
    }

    # 整個區段,並保留路徑的其餘部分
    location /blog/ {
        rewrite ^/blog/(.*)$ /articles/$1 permanent;
    }
}

$request_uri 包含原始的路徑和查詢字串,因此不會遺失任何資訊。rewrite ... permanent 會送出 301,rewrite ... redirect 則送出 302;若要使用 307 或 308,請搭配該狀態碼使用 return。

Apache(.htaccess)

Apache
# mod_alias:單一網頁,以及整個資料夾(路徑的其餘部分會附加在後面)
Redirect 301 /old-page https://example.com/new-page
Redirect 301 /blog/ https://example.com/articles/

# 搭配規則運算式的 mod_alias
RedirectMatch 301 ^/products/([0-9]+)\.html$ https://example.com/p/$1

# mod_rewrite:所有 http:// 與 www 要求都只經過一個躍點就導向 https://example.com
RewriteEngine On
RewriteCond %{HTTPS} off [OR]
RewriteCond %{HTTP_HOST} ^www\. [NC]
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]

沒有指定狀態碼的 Redirect 會送出 302,因此請務必寫上狀態碼。如果 Apache 位於以一般 HTTP 連線的負載平衡器或 CDN 後方,%{HTTPS} 會一直是 off,最後一條規則就會形成迴圈;請改為檢查 %{HTTP:X-Forwarded-Proto}。mod_alias 和 mod_rewrite 各自獨立運作,檔案中各行的先後順序,並不能決定哪個模組先執行。同時符合兩個模組規則的網址(例如這裡的 http://www.example.com/old-page)可能會經過兩個躍點;對於必須在一個躍點內完成重新導向的網址,請將它們的所有規則都放在同一個模組中。

Cloudflare

Cloudflare 可以在要求抵達您的伺服器之前,就在邊緣完成重新導向:

  • Single Redirects(儀表板中的 Redirect Rules 區段)會以篩選運算式比對要求,並以 301、302、303、307 或 308 將要求重新導向至靜態或動態目標。一條典型、從 www 重新導向至根網域的規則,設定如下:

    When incoming requests match:  (http.host eq "www.example.com")
    URL redirect type:             Dynamic
    Expression:                    concat("https://example.com", http.request.uri.path)
    Status code:                   301
    Preserve query string:         On
  • Bulk Redirects 可處理大量來源網址與目標網址的清單,例如以 CSV 上傳的搬遷對照表。每個項目都有各自的狀態碼和選項(例如保留查詢字串),再由一條 Bulk Redirect Rule 啟用整份清單。

  • Workers 可以在程式碼中使用 Response.redirect() 建立重新導向:

JavaScript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === '/old-page') {
      url.pathname = '/new-page'; // 查詢字串會保留
      return Response.redirect(url.toString(), 301);
    }
    return fetch(request);
  },
};

Response.redirect() 預設使用 302,而且只接受 301、302、303、307 和 308。使用其他任何狀態碼都會擲回 RangeError。

Node.js 與 Express

JavaScript
import express from 'express';

const app = express();
app.set('trust proxy', 1); // 在負載平衡器或 CDN 後方執行時

// 只用一個躍點處理 HTTPS 與標準主機
app.use((req, res, next) => {
  if (req.secure && req.hostname === 'example.com') return next();
  res.redirect(301, `https://example.com${req.originalUrl}`);
});

// 單一搬移的網頁
app.get('/old-page', (req, res) => {
  res.redirect(301, '/new-page');
});

未指定狀態碼的 res.redirect(url) 會送出 302。Express 5 只支援 res.redirect(status, url) 這種參數順序。在純 Node.js 中,則需要自行設定狀態碼和標頭:

JavaScript
res.writeHead(301, { Location: 'https://example.com/new-page' });
res.end();

PHP

PHP
<?php
// 必須在送出任何輸出之前執行。
header('Location: https://example.com/new-page', true, 301);
exit;

只設定 Location 標頭時,PHP 會送出 302(除非先前已設定 201 或 3xx 狀態碼),因此請將狀態碼作為第三個引數傳入,並呼叫 exit,避免後續程式碼繼續執行。在 WordPress 中,wp_redirect() 和 wp_safe_redirect() 的預設值同樣是 302;請將 301 作為第二個引數傳入。

選擇正確的重新導向狀態碼

  • 頁面已永久搬移,而且是以 GET 載入的一般頁面:使用 301。使用 308 也同樣可行。
  • 搬移是永久的,而且用戶端可能會送出 POST、PUT 或 DELETE,例如 API、表單動作和 webhook:使用 308。
  • 重新導向是暫時的,而且只需考慮 GET:使用 302。
  • 重新導向是暫時的,而且必須保留要求方法:使用 307。
  • 表單或其他 POST 要求已處理完成,且應讓使用者看到結果頁面:使用 303。
  • 從 HTTP 重新導向至 HTTPS:使用 301 或 308,並從 HTTPS 網站送出 Strict-Transport-Security 標頭,讓瀏覽器下次略過不安全的要求。
  • 內容已不存在,也沒有對應的替代內容:不要重新導向,請回傳 404 或 410。Google 可能會將重新導向至不相關頁面(例如首頁)的情況視為軟性 404(soft 404)。
  • 300、305 和 306:請勿使用。304 是伺服器或快取針對條件式要求所產生的回應,永遠不是重新導向的選項。

301 與 308 的快取陷阱

由於 301 和 308 可依經驗法則快取,瀏覽器可能會將它們儲存起來,不再詢問伺服器。以 Chromium 為基礎的瀏覽器,會一直保留沒有快取標頭的 301 或 308,直到該項目被移出快取,或使用者清除快取為止。錯誤的永久重新導向即使在伺服器修正之後很久,仍可能持續把曾經造訪的訪客送到錯誤的地方,而且您無法可靠地從他們的瀏覽器中撤回。

為了安全起見:

  • 先用 302 或 307 測試新規則,確認目的地無誤後,再改為 301 或 308。
  • 或者,在逐步推出期間,限制永久重新導向可被快取的時間:
nginx
location = /old-page {
    add_header Cache-Control "max-age=3600";
    return 301 https://example.com/new-page;
}
  • 測試時,請使用無痕視窗、開啟 Disable cache(停用快取)的 DevTools,或是完全不使用瀏覽器快取的伺服器端檢查工具。
  • 變更重新導向後,請清除 CDN 和代理伺服器的快取,因為它們也可能儲存重新導向。
  • 不要試圖將 B 重新導向回 A,來撤銷已被快取的 A → B 重新導向。瀏覽器已快取第一次重新導向的訪客,可能會因此陷入迴圈。

測試您的重新導向

無論使用哪種平台,都請從外部驗證結果。301 重新導向檢查工具可確認永久搬移的每個躍點都是 301 或 308;重新導向鏈檢查工具會顯示每個躍點及其 Location 和標頭;HTTPS 重新導向檢查工具則能一次測試全部四種通訊協定與主機變體。Meta Refresh 重新導向檢查工具可以找出只讀取標頭的工具所遺漏的用戶端重新導向,重新導向延遲檢查工具則會測量每個躍點耗費的時間。

在終端機中執行 curl -sIL https://example.com/old-page,即可印出每個躍點的標頭。我們的如何檢查網址重新導向指南,會詳細介紹命令列與 DevTools 的用法,以及常見的重新導向問題。