Danh sách bài viết

Bài 116: `route.fetch()` Với `maxRedirects` [v1.31]

Chương C đào sâu network: routing advanced, HAR, WebSocket, API testing. C.1 bắt đầu với route.fetch() options. Bài này tập trung vào maxRedirects [v1.31+] — option kiểm soát bao nhiêu lần redirect được follow khi route.fetch() đi server thật, phục vụ việc test redirect logic mà không cần để browser tự xử lý.

28/05/2026
0 lượt xem
1

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: N và N có ý nghĩa gì.
  • Viết test verify redirect đúng bằng cách inspect status code và Location header.
  • Phân biệt maxRedirects trong route.fetch() với redirect behavior của page.goto().
  • Tránh được 4 pitfall phổ biến khi dùng maxRedirects.
2

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).

3

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.

4

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.

5

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 Location header để 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();
});
6

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 });
});
7

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).

8

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 });
});
9

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);
});
10

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 });
});
11

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 });
  }
});
12

Pitfalls

  • Nhầm maxRedirects trong route.fetch() với redirect trong page.goto(). maxRedirects chỉ kiểm soát route.fetch(). page.goto() có option riêng là waitUntil, không có maxRedirects. Dùng maxRedirects: 0 trong route.fetch() không ngăn browser follow redirect sau khi handler gọi route.fulfill().

  • maxRedirects: 0 nhưng quên handle hoặc fulfill response 3xx. Khi route.fetch() trả response 3xx với maxRedirects: 0, handler vẫn phải gọi route.fulfill() (hoặc route.abort(), hoặc route.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ó maxRedirects sẽ follow mãi đến khi timeout. Trong môi trường test, dùng maxRedirects: N hợp lý (ví dụ maxRedirects: 10) hoặc cặp đôi với timeout để giới hạn thời gian chờ. Bài 118 đi sâu vào option timeout.

13

Tổng Kết

  • maxRedirects là option của route.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: 0 trả 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: 0 assert 301 vs 302, assert location header.
  • maxRedirects chỉ ảnh hưởng lần fetch trong route.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ọi fulfill, abort hoặc continue — dù response là 2xx hay 3xx.
14

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']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.

15

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.

Bài 117: route.fetch() Với maxRetries [v1.46]