Mục lục
- Mục Tiêu Bài Học
- Recap
route.fetch() - Option
maxRedirects[v1.31+] - Default Behavior: Follow Redirect Tự Động
maxRedirects: 0— Dừng Ở Response 3xxmaxRedirects: N— Follow Tối Đa N Lần- Pattern Test Redirect Logic
- Inspect Location Header
- Pattern Test SEO Redirect
- Combine maxRedirects Với fulfill
- Khác Page Navigation Redirect
- Pitfalls
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài này, bạn sẽ:
- Giải thích được default behavior của
route.fetch()khi server trả 3xx: follow redirect hay dừng lại. - Sử dụng đúng
maxRedirects: 0để nhận response 3xx thay vì response của destination. - Hiểu khi nào dùng
maxRedirects: Nvà N có ý nghĩa gì. - Viết test verify redirect đúng bằng cách inspect status code và
Locationheader. - Phân biệt
maxRedirectstrongroute.fetch()với redirect behavior củapage.goto(). - Tránh được 4 pitfall phổ biến khi dùng
maxRedirects.
Recap route.fetch()
route.fetch() thực hiện request đến server thật trong khi bạn đang intercept, trả về APIResponse để inspect hoặc modify trước khi fulfill về browser. Pattern cơ bản (đã cover ở Series 1, nhóm 34 — bài 297):
await page.route('**/api/data', async (route) => {
const response = await route.fetch(); // fetch real response
const json = await response.json();
json.modified = true;
await route.fulfill({ json }); // trả về browser đã modify
});
route.fetch() nhận một optional options object với các field tương tự route.continue(): url, method, headers, postData. Từ v1.31, object này có thêm maxRedirects. Từ v1.33 thêm timeout. Từ v1.46 thêm maxRetries.
Bài này chỉ đi vào maxRedirects. Các option còn lại được cover ở bài 117 (maxRetries) và bài 118 (timeout).
Option maxRedirects [v1.31+]
maxRedirects là số nguyên không âm kiểm soát số redirect route.fetch() sẽ follow trước khi dừng lại và trả response hiện tại:
// Cú pháp
const response = await route.fetch({
maxRedirects: 0, // không follow redirect nào
});
const response = await route.fetch({
maxRedirects: 5, // follow tối đa 5 redirect
});
// Không truyền → default behavior
const response = await route.fetch();
Giá trị hợp lệ:
0— không follow redirect, trả response 3xx đầu tiên.N(N > 0) — follow tối đa N redirect; nếu redirect chain ngắn hơn N thì follow hết chain và trả response cuối.- Không truyền hoặc
undefined— default behavior (follow tất cả redirect tự động).
Option này được thêm vào Playwright v1.31 (tháng 3/2023). Version cũ hơn không nhận option này — nếu truyền vào, nó bị ignored silently, không throw error.
Default Behavior: Follow Redirect Tự Động
Khi không truyền maxRedirects, route.fetch() follow redirect tự động giống như browser thông thường. Nếu server trả 301 /old-path → /new-path, route.fetch() tự request tiếp /new-path và trả response của destination:
await page.route('**/old-path', async (route) => {
const response = await route.fetch();
// response ở đây là response của /new-path (200 OK)
// KHÔNG phải response 301 của /old-path
console.log(response.status()); // 200
await route.fulfill({ response });
});
Đây là behavior hợp lý cho đa số trường hợp — test nhận nội dung cuối cùng mà browser hiển thị, không cần quan tâm chain redirect ở giữa. Nhưng khi muốn test bản thân redirect (kiểm tra server có redirect không, redirect đến đúng URL không), behavior này lại che đi thông tin cần thiết.
maxRedirects: 0 — Dừng Ở Response 3xx
maxRedirects: 0 khiến route.fetch() dừng ngay khi nhận response đầu tiên — kể cả nếu đó là 3xx. Test nhận response 3xx thật với đầy đủ headers, bao gồm Location:
await page.route('**/old-path', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
console.log(response.status()); // 301
console.log(response.headers()['location']); // '/new-path'
await route.fulfill({ response });
});
Thời điểm nên dùng maxRedirects: 0:
- Muốn assert server có trả 3xx không (verify redirect đang hoạt động).
- Muốn inspect
Locationheader để kiểm tra redirect đến đúng URL. - Muốn phân biệt 301 permanent vs 302 temporary.
- Muốn test behavior của app khi nhận thủ công 3xx response (edge case).
Lưu ý: khi fulfill { response } về browser với status 3xx, browser sẽ tự follow redirect đó. Nếu chỉ muốn kiểm tra mà không để browser follow, dùng route.abort() sau khi đã kiểm tra xong:
await page.route('**/old-path', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
// Kiểm tra redirect
expect(response.status()).toBe(301);
expect(response.headers()['location']).toBe('/new-path');
// Không fulfill về browser — abort request ở đây
await route.abort();
});
maxRedirects: N — Follow Tối Đa N Lần
maxRedirects: N (N > 0) cho phép follow tối đa N redirect. Sau N lần follow, nếu server vẫn trả 3xx thì trả response 3xx đó về (không follow tiếp):
// Server chain: /a → /b → /c → /d (3 redirect)
await page.route('**/a', async (route) => {
const response = await route.fetch({ maxRedirects: 1 });
// Follow 1 lần: /a → /b
// /b vẫn redirect về /c → dừng, trả response của /b (3xx)
console.log(response.status()); // 3xx
console.log(response.headers()['location']); // '/c'
await route.fulfill({ response });
});
await page.route('**/a', async (route) => {
const response = await route.fetch({ maxRedirects: 3 });
// Follow 3 lần: /a → /b → /c → /d
// /d trả 200 → đây là response cuối
console.log(response.status()); // 200
await route.fulfill({ response });
});
Use case thực tế của maxRedirects: N: inspect response ở điểm giữa của redirect chain — ví dụ verify redirect bước 2 đi đúng URL trước khi đến destination cuối cùng.
// Verify redirect chain: /legacy → /v1 → /v2/endpoint
await page.route('**/legacy', async (route) => {
// Follow 1 lần để thấy response của /v1
const midResponse = await route.fetch({ maxRedirects: 1 });
expect(midResponse.status()).toBe(302);
expect(midResponse.headers()['location']).toBe('/v2/endpoint');
// Sau khi verify chain, fulfill với response đầy đủ
const fullResponse = await route.fetch(); // không maxRedirects → follow hết
await route.fulfill({ response: fullResponse });
});
Pattern Test Redirect Logic
Pattern đơn giản nhất để verify redirect — intercept, fetch với maxRedirects: 0, assert status và Location:
import { test, expect } from '@playwright/test';
test('verify /old-path redirect về /new-path với 301', async ({ page }) => {
let redirectStatus: number | null = null;
let redirectLocation: string | null = null;
await page.route('**/old-path', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
redirectStatus = response.status();
redirectLocation = response.headers()['location'] ?? null;
// Fulfill response 3xx về browser (browser sẽ tự follow)
await route.fulfill({ response });
});
await page.goto('/old-path');
// Assert redirect đã xảy ra
expect(redirectStatus).toBe(301);
expect(redirectLocation).toBe('/new-path');
// Assert browser đã follow redirect đến đúng URL cuối
await expect(page).toHaveURL(/\/new-path/);
});
Pattern này tách biệt hai việc: kiểm tra server có trả 301 không (qua maxRedirects: 0), và kiểm tra browser đã navigate đến đúng destination (qua toHaveURL). Cả hai có thể fail độc lập, giúp xác định lỗi nằm ở server (sai status / Location) hay ở client (sai xử lý redirect).
Inspect Location Header
Location header trong response 3xx chứa URL destination của redirect. Giá trị này có thể là absolute URL hoặc relative path tùy server:
await page.route('**/api/old', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
const headers = response.headers();
// Location là lowercase trong Playwright APIResponse.headers()
const location = headers['location'];
console.log(location);
// Absolute: 'https://example.com/api/new'
// Relative: '/api/new'
// Path-only: 'new'
await route.fulfill({ response });
});
Lưu ý: response.headers() trong Playwright trả về object với tất cả header names đã lowercase. Dùng 'location' (chữ thường), không phải 'Location'.
Khi cần inspect nhiều header của response redirect:
await page.route('**/old', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
if (response.status() >= 300 && response.status() < 400) {
const { location, 'cache-control': cacheControl } = response.headers();
console.log('Redirect to:', location);
console.log('Cache-Control:', cacheControl);
// 301 permanent thường đi kèm Cache-Control: max-age=...
// 302 temporary thường đi kèm Cache-Control: no-cache
}
await route.fulfill({ response });
});
Pattern Test SEO Redirect
Redirect là yếu tố quan trọng trong SEO. 301 (permanent) truyền link equity, 302 (temporary) không truyền. Sai loại redirect gây hậu quả về ranking mà khó phát hiện bằng test thông thường. route.fetch({ maxRedirects: 0 }) cho phép verify chính xác loại redirect:
import { test, expect } from '@playwright/test';
// Danh sách redirect cần verify — thường lấy từ sitemap hoặc spreadsheet
const permanentRedirects = [
{ from: '/blog/old-post-url', to: '/blog/new-post-url' },
{ from: '/products/legacy', to: '/shop/products' },
{ from: '/about-us', to: '/about' },
];
for (const { from, to } of permanentRedirects) {
test(`301 permanent redirect: ${from} → ${to}`, async ({ page }) => {
let capturedStatus: number | null = null;
let capturedLocation: string | null = null;
await page.route(`**${from}`, async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
capturedStatus = response.status();
capturedLocation = response.headers()['location'] ?? null;
await route.fulfill({ response });
});
await page.goto(from);
expect(capturedStatus).toBe(301);
// Location phải chứa đường dẫn đến
expect(capturedLocation).toContain(to);
// Destination phải có nội dung (không phải trang lỗi)
await expect(page).not.toHaveURL(/404/);
});
}
test('verify 302 temporary redirect (không phải 301)', async ({ page }) => {
let capturedStatus: number | null = null;
await page.route('**/promo/summer', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
capturedStatus = response.status();
await route.fulfill({ response });
});
await page.goto('/promo/summer');
// Promo redirect nên là 302, không phải 301
// 301 sẽ bị browser cache → link promo sau đó không thể đổi destination
expect(capturedStatus).toBe(302);
});
Combine maxRedirects Với fulfill
maxRedirects chỉ ảnh hưởng response mà route.fetch() trả về — nó không ảnh hưởng response bạn fulfill về browser. Sau khi nhận response với maxRedirects, bạn có thể fulfill về browser theo nhiều cách:
Fulfill nguyên xi response 3xx. Browser nhận response 3xx và tự follow redirect:
await page.route('**/old', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
// Browser nhận 301/302, tự follow đến Location
await route.fulfill({ response });
});
Inspect rồi fulfill response khác. Fetch với maxRedirects: 0 để inspect, sau đó fetch lại không có maxRedirects để lấy response cuối cùng:
await page.route('**/old', async (route) => {
// Bước 1: inspect redirect
const redirectResponse = await route.fetch({ maxRedirects: 0 });
expect(redirectResponse.status()).toBe(301);
expect(redirectResponse.headers()['location']).toBe('/new');
// Bước 2: fetch response cuối và fulfill về browser
const finalResponse = await route.fetch(); // follow redirect
await route.fulfill({ response: finalResponse });
});
Modify response sau khi follow redirect.
await page.route('**/old', async (route) => {
// Follow redirect và nhận response cuối
const response = await route.fetch({ maxRedirects: 5 });
const json = await response.json();
json.redirectedFrom = '/old'; // inject thêm field
await route.fulfill({ json });
});
Khác Page Navigation Redirect
maxRedirects trong route.fetch() chỉ kiểm soát behavior của lần HTTP request trong route.fetch() — nó không ảnh hưởng đến cách browser navigate sau khi nhận response từ route.fulfill().
Timeline:
1. page.goto('/old') ← browser request /old
2. page.route handler kích hoạt
3. route.fetch({ maxRedirects: 0 }) ← Playwright request /old đến server
server trả 301 → /new
fetch DỪNG ở đây, trả response 301
4. route.fulfill({ response }) ← browser nhận response 301 từ handler
5. Browser thấy 301 + Location ← browser TỰ navigate đến /new
6. page.goto() resolves tại /new
Nói cách khác: maxRedirects: 0 ngăn Playwright follow redirect trong nội bộ route.fetch(), nhưng nếu bạn fulfill response 3xx đó về browser, browser vẫn follow redirect bình thường.
Để thực sự ngăn navigation đến URL mới (ví dụ test behavior khi server redirect nhưng client không muốn follow), dùng route.abort() thay vì route.fulfill:
await page.route('**/old', async (route) => {
const response = await route.fetch({ maxRedirects: 0 });
// Chỉ inspect, không fulfill — abort thay thế
if (response.status() === 301) {
expect(response.headers()['location']).toBe('/new');
await route.abort(); // browser không navigate thêm
} else {
await route.fulfill({ response });
}
});
Pitfalls
-
Nhầm
maxRedirectstrongroute.fetch()với redirect trongpage.goto().maxRedirectschỉ kiểm soátroute.fetch().page.goto()có option riêng làwaitUntil, không cómaxRedirects. DùngmaxRedirects: 0trongroute.fetch()không ngăn browser follow redirect sau khi handler gọiroute.fulfill(). -
maxRedirects: 0nhưng quên handle hoặc fulfill response 3xx. Khiroute.fetch()trả response 3xx vớimaxRedirects: 0, handler vẫn phải gọiroute.fulfill()(hoặcroute.abort(), hoặcroute.continue()). Nếu handler kết thúc mà không gọi bất kỳ method nào, request sẽ treo cho đến khi timeout. Không có magic auto-fulfill.// ANTI-PATTERN — handler không fulfill await page.route('**/old', async (route) => { const response = await route.fetch({ maxRedirects: 0 }); expect(response.status()).toBe(301); // Quên gọi route.fulfill / abort / continue → request hang }); // page.goto('/old') sẽ timeout -
Dùng trên Playwright version < v1.31. Option
maxRedirectsđược thêm vào v1.31. Trên version cũ hơn, option bị ignored silently —route.fetch()vẫn follow redirect tự động như default, test không báo lỗi nhưng kết quả không như kỳ vọng. Kiểm tra version:npx playwright --version. -
Redirect chain quá dài gây timeout. Nếu server có redirect loop (A → B → A) hoặc chain dài bất thường,
route.fetch()không cómaxRedirectssẽ follow mãi đến khi timeout. Trong môi trường test, dùngmaxRedirects: Nhợp lý (ví dụmaxRedirects: 10) hoặc cặp đôi vớitimeoutđể giới hạn thời gian chờ. Bài 118 đi sâu vào optiontimeout.
Tổng Kết
maxRedirectslà option củaroute.fetch(), thêm từ v1.31. Giá trị0= không follow redirect nào;N= follow tối đa N lần; không truyền = follow tất cả (default).- Default:
route.fetch()follow redirect tự động — trả response của destination, không phải response 3xx trung gian. maxRedirects: 0trả nguyên response 3xx đầu tiên với đầy đủ headers (location,cache-control, v.v.). Dùng để verify redirect logic: status code đúng loại, Location header đúng URL.- Pattern SEO redirect: loop qua danh sách URL cần verify, dùng
maxRedirects: 0assert 301 vs 302, assertlocationheader. maxRedirectschỉ ảnh hưởng lần fetch trongroute.fetch(). Nếu handler fulfill response 3xx về browser, browser vẫn tự follow redirect đó.- Sau khi
route.fetch(), handler BẮT BUỘC gọifulfill,aborthoặccontinue— dù response là 2xx hay 3xx.
Bài Tập Củng Cố
Câu 1
Server trả 301 /old → /new → /final (hai redirect). Gọi route.fetch() không truyền maxRedirects. response.status() trả về bao nhiêu? response.headers()['location'] có giá trị gì?
Đáp án
response.status() trả 200 (hoặc status thực của /final). Không truyền maxRedirects → default follow tất cả redirect → Playwright tự follow /old → /new → /final, response trả về là response của /final. Response của /final là 200 (không phải 3xx), nên không có location header. response.headers()['location'] là undefined.
Câu 2
Bạn dùng route.fetch({ maxRedirects: 0 }) và nhận response 301. Sau đó gọi route.fulfill({ response }). page.goto() cuối cùng resolve ở URL nào?
Đáp án
Browser nhận response 301 từ route.fulfill — browser thấy status 3xx và Location header, nên tự follow redirect đến URL đó. page.goto() resolve ở URL destination của redirect (URL trong Location header). maxRedirects: 0 chỉ ngăn Playwright follow trong nội bộ route.fetch(), không ngăn browser follow sau khi nhận response.
Câu 3
Handler intercept /promo, dùng route.fetch({ maxRedirects: 0 }), kiểm tra status xong rồi không gọi gì thêm. Điều gì xảy ra?
Đáp án
Request bị treo. Khi handler kết thúc mà không gọi route.fulfill(), route.abort() hay route.continue(), Playwright không tự động xử lý request đó. Browser tiếp tục đợi response cho đến khi navigation timeout (mặc định 30s với Test Runner). Test sẽ fail với lỗi timeout. BẮT BUỘC phải gọi một trong ba method trên ở cuối handler.
Câu 4
Playwright version đang dùng là v1.28. Bạn thêm maxRedirects: 0 vào route.fetch(). Test chạy không có lỗi compile, status log ra là 200. Điều gì đã xảy ra?
Đáp án
Option maxRedirects bị ignored silently. maxRedirects chỉ có hiệu lực từ v1.31. Trên v1.28, Playwright không nhận ra option này và bỏ qua, không throw error. route.fetch() chạy với default behavior — follow tất cả redirect — nên response trả về là response của destination (200). Test không báo lỗi nhưng không thực sự test được redirect logic. Cần upgrade lên ít nhất v1.31 để maxRedirects hoạt động.
Bài Tiếp Theo
Bài 117 tiếp tục với option maxRetries [v1.46] — kiểm soát số lần retry khi route.fetch() gặp lỗi mạng.
