儲存完資料後呼叫 redirect('/done'),頁面卻沒有移動,反而在日誌看到 NEXT_REDIRECT。若程式把整段儲存與導向包在同一個 try/catch,先檢查 catch是否把導向攔住了。Next.js的 redirect() 會透過拋出例外結束目前處理;這個例外需要交回框架,才能完成導向。
這種情況不必先改網址或重做成功頁。把「處理儲存錯誤」和「成功後導向」分開,通常就能看出差別。本文用 App Router的 Route Handler 做最小案例,再說明 Server Action和客戶端按鈕該怎麼選,避免把不同情境的回應狀態混在一起。
這個catch也會接住redirect
下面的 try包含兩件事:執行一個示範工作,與呼叫導向。示範工作只返回字串,沒有接資料庫;我們要觀察的是控制流程,而不是儲存功能。
import { redirect } from 'next/navigation'
export async function GET() {
try {
await Promise.resolve('saved')
redirect('/done')
} catch (error) {
return Response.json({ error: error.message })
}
}
這段可放在測試專案的 app/bad/route.js。看起來像任何錯誤都能轉成 JSON,問題也正出在「任何」:導向本身拋出的例外同樣落入這個 catch。回傳 JSON後,框架收到的是你的正常回應,導向控制流程已經被改掉。
此時使用者可能看到錯誤文字,網路面板卻顯示 200。原因是 Response.json沒有指定其他狀態碼時,產生的就是預設正常回應;它不會因為內容有 error欄位,就自動成為 HTTP錯誤。這只是這個範例的結果,不是 Next.js規定所有被捕捉的例外都回200。
不要用 error.message === 'NEXT_REDIRECT' 當作整個應用程式的固定分類規則,也不要手動回傳某個內部 digest格式冒充框架導向。內部控制訊號不是給你拼接的回應 API。最容易理解的修正是縮小 try的範圍,讓成功路徑直接呼叫框架公開的導向方法。
讓redirect在try外執行
把需要捕捉的工作留在 try內,失敗時回傳失敗結果;只有工作完成,才會走到外面的 redirect。不要在 catch後不分成功與失敗就一律導向。
import { redirect } from 'next/navigation'
export async function GET() {
try {
await Promise.resolve('saved')
} catch (error) {
return Response.json(
{ error: 'save failed' },
{ status: 500 }
)
}
redirect('/done')
}
放在 app/good/route.js,並建立 app/done/page.js 作為目標頁。兩個 Route Handler唯一重要差別,是導向有沒有在會吞例外的 try範圍內。實際專案要把 Promise.resolve換成你的工作,且確認失敗分支確實停止,不會繼續走到成功頁。
這裡使用 GET只是為了方便看導向回應。會新增訂單或修改設定的真實操作,應用合適的 Server Action或 POST處理,不應因為範例好測,就改成讓任意 GET請求寫入資料。驗證輸入、權限與寫入交易也仍是原業務流程的一部分;將 redirect移出去並不會替你補上這些要求。
用未追隨導向的請求確認結果
在自有測試專案啟動 Next.js,分別請求 /bad和 /good。如果工具自動追隨導向,最後可能只看到 /done的正常頁,反而漏掉最關鍵的中間回應。用 curl時不加 -L,先看第一跳。
curl -i http://127.0.0.1:3000/bad
curl -i http://127.0.0.1:3000/good
本文的隔離案例使用 Next.js 16.3.8與 React 19.3.0。錯誤寫法回200,沒有 Location,內容是 {"error":"NEXT_REDIRECT"};修正寫法回307,Location為 /done。這組比較直接指出 catch改變了控制流程,不需要把應用程式日誌裡所有例外都認定為儲存失敗。
也可以在瀏覽器直接開 /good,確認網址移到成功頁,再看開發者工具的 Network紀錄。兩種觀察用途不同:第一跳標頭回答伺服器有沒有送導向,瀏覽器則回答目前導覽是否抵達目標。如果目標頁本身出錯,那是另一個問題;修正這個 catch不會修好不存在的路由。
倘若把相同處理包在更外層的共用函式,而外層又有廣泛 catch,導向仍可能在外層被吞掉。修稿時要沿呼叫鏈看完,不是只把一行移出最近的 try就結束。要由共用層捕捉哪些業務錯誤,應清楚訂定;框架的控制例外不要被轉成通用成功回應。
Server Action的303與這個307不同
現行 Next.js官方 redirect文件區分呼叫方式:Server Action在有JavaScript時執行客戶端導覽;漸進增強的表單提交回303,其他情境回307。本文的真案例是 Route Handler,因此看到307合理,不應照抄成「所有 action請求都會回相同狀態」。串流回應情境也可能插入客戶端導向用的 meta標記,不能只看單一頁面請求套用同一判斷。
Server Action中,先完成要捕捉錯誤的變更,成功後再在 try外導向。同樣要避免把導向包進一個會回傳錯誤物件的 catch。若需要重新驗證快取或更新畫面資料,依實際頁面邏輯安排;redirect是結束目前處理的操作,放在它後面的程式不應被當成必定執行的下一步。
// 示意既有Server Action的成功路徑:
try {
await saveChanges()
} catch (error) {
return { error: '無法儲存,請稍後再試' }
}
redirect('/done')
這段 saveChanges是代表你自己的工作,不是 Next.js內建函式。整個 Server Action還需適當的 'use server'、匯入與表單接法;不能只貼這幾行就聲稱完成登入、付款或權限檢查。對預期會發生的驗證錯誤,Next.js官方建議以回傳值處理,讓呼叫端能呈現訊息;非預期故障則另依錯誤邊界處理。
如果是Client Component的按鈕事件
redirect可用於 Client Component的渲染情境,但不是按鈕事件處理器的替代品。若使用者按「返回清單」而你只需要客戶端導覽,在 Client Component裡使用 useRouter的 router.push,或用合適的 Link。官方文件把這些用途分開說明。
'use client'
import { useRouter } from 'next/navigation'
export default function BackButton() {
const router = useRouter()
return (
<button type="button"
onClick={() => router.push('/items')}>
返回清單
</button>
)
}
目的網址若來自輸入或查詢參數,要驗證是否符合應用程式允許的路徑,不能直接把任意內容丟給 router.push。官方 API特別提醒不可信網址的風險。對站內固定頁,直接使用已知的相對路徑較容易核對;需要允許外部導向時,再明確設計允許清單。
搬家或永久改網址則是另一個需求。permanentRedirect使用308,會有永久導向的語意;不要為了「讓它確實跳」把正常成功頁一律改成永久導向。先選正確情境,再確認 try範圍、第一跳回應、目標頁是否存在,就能把伺服器控制流程與瀏覽器導覽分開查清楚。
確認成功後,也要試一次真的工作失敗
只看成功導向仍可能漏掉另一個問題:外層 catch雖然移對位置,業務失敗卻沒有結束執行。可在專用測試專案把示範工作換成 Promise.reject(new Error('demo failure')),保留同樣的失敗回傳,再觀察是否出現500與失敗內容,而且沒有成功頁的 Location。這是讀者可以補做的失敗分支練習,應與本文已列出的兩個成功工作案例分開紀錄。
如果真實工作不是拋例外,而是回傳 { ok: false },try本身不會把它當成例外。你要依該服務的介面檢查回傳值,決定是否返回錯誤;不能只因為 await正常結束就導向。這個差別與 redirect是否被 catch吞掉無關,卻經常在修正後浮現,因此應一起核對成功條件。
最後檢查測試請求的起點。瀏覽器網址導覽、表單提交、JavaScript fetch不是相同的使用方式。fetch收到或追隨 HTTP導向,並不等於整個瀏覽器頁面會一起換網址;若你用的是客戶端 API呼叫,要在相應的 UI流程處理結果。先記下實際呼叫方式,再把狀態碼與畫面行為對上,才不會伺服器已正確回應,卻一直在錯誤的層次改程式。

評論0