Mục lục
Mục Tiêu Bài Học
Sau khi hoàn thành bài này, bạn sẽ:
- Biết cú pháp
route.fetch({ maxRetries })và giá trị default (v1.46+). - Hiểu rõ điều kiện retry: network error ở tầng connection (ECONNRESET, ECONNREFUSED, drop) — không phải HTTP error (4xx/5xx).
- Phân biệt
maxRetriescủaroute.fetch()với test-level retries — hai cơ chế hoàn toàn khác nhau về phạm vi và mục đích. - Áp dụng pattern resilient fetch cho staging backend không ổn định.
- Tránh 4 pitfall phổ biến khi dùng
maxRetries.
Cú Pháp và Default
maxRetries là option của route.fetch(), được thêm từ Playwright v1.46.
const response = await route.fetch({ maxRetries: 3 });
Khi gọi như trên, nếu route.fetch() gặp network error, Playwright sẽ tự động thử lại tối đa 3 lần trước khi ném lỗi ra ngoài.
Default: maxRetries: 0 — không retry. Nếu không truyền option này, behavior không thay đổi so với các version trước v1.46.
// Các cách truyền tương đương
const r1 = await route.fetch(); // maxRetries: 0 (default)
const r2 = await route.fetch({ maxRetries: 0 }); // tường minh — không retry
const r3 = await route.fetch({ maxRetries: 3 }); // retry tối đa 3 lần
Option này có thể kết hợp với các option khác của route.fetch() như headers, postData, url, timeout:
const response = await route.fetch({
maxRetries: 3,
headers: { 'X-Custom': 'value' },
timeout: 10_000,
});
Retry Khi Nào — Network Error vs HTTP Error
Điểm quan trọng nhất và hay gây nhầm lẫn nhất: maxRetries chỉ retry network error, không retry HTTP error.
Network error — sẽ retry
Network error xảy ra khi kết nối TCP không thiết lập được hoặc bị gián đoạn trước khi nhận được HTTP response:
ECONNRESET— kết nối bị reset bởi peer (server đóng connection đột ngột)ECONNREFUSED— server từ chối kết nối (port không mở, service chưa sẵn sàng)ENOTFOUND— DNS không phân giải được hostname- Connection drop — mạng bị ngắt giữa chừng
- Socket timeout — chờ connection quá lâu không thiết lập được
Trong các trường hợp trên, không có HTTP response nào được trả về — route.fetch() sẽ ném một lỗi JavaScript. Với maxRetries: N, Playwright retry tối đa N lần trước khi để lỗi propagate.
HTTP error — không retry
HTTP error (4xx, 5xx) là các response hợp lệ về mặt network: server đã nhận request, xử lý, và trả về status code chỉ ra lỗi ứng dụng hoặc server. Về mặt kết nối mạng, đây là thành công — route.fetch() nhận được response bình thường và trả về:
await page.route('**/api/data', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
// response.status() = 500 → KHÔNG retry
// response.status() = 404 → KHÔNG retry
// response.status() = 200 → không cần retry
await route.fulfill({ response });
});
Bảng so sánh:
| Loại lỗi | Ví dụ | maxRetries có retry không? |
|---|---|---|
| Network error | ECONNRESET, ECONNREFUSED, drop | Có — retry đến maxRetries lần |
| HTTP 4xx | 404 Not Found, 403 Forbidden | Không — trả về response 4xx |
| HTTP 5xx | 500 Internal Server Error, 503 | Không — trả về response 5xx |
| HTTP 2xx/3xx | 200 OK, 201 Created, 302 | Không cần — trả về response thành công |
Lý do thiết kế: 5xx cho thấy server đang chạy và phản hồi — chỉ là phản hồi lỗi. Retry 5xx tại tầng network thường không giải quyết được gì và có thể gây side effect (duplicate request). Nếu cần retry HTTP error, logic đó thuộc về app code hoặc test code, không phải network layer.
Khác Biệt Với Test-Level Retries
Playwright có hai cơ chế retry hoàn toàn khác nhau về phạm vi:
Test-level retries (retries trong config)
Khi test fail vì bất kỳ lý do gì, Playwright chạy lại toàn bộ test từ đầu — bao gồm tất cả setup, actions, assertions, teardown. Mỗi lần retry là một lần chạy mới của test function.
// playwright.config.ts
export default defineConfig({
retries: 2, // retry cả test tối đa 2 lần nếu fail
});
route.fetch({ maxRetries })
Chỉ retry một HTTP request cụ thể trong route handler — không chạy lại test, không chạy lại bất kỳ action nào khác trước đó. Test tiếp tục bình thường sau khi fetch thành công (hoặc ném lỗi nếu tất cả retry đều fail).
// Test chạy 1 lần duy nhất — maxRetries chỉ retry fetch bên trong handler
await page.route('**/api/data', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
// Nếu attempt 1 → ECONNRESET → Playwright retry ngay lập tức
// Nếu attempt 2 → ECONNRESET → Playwright retry
// Nếu attempt 3 → OK → trả về response
// Test tiếp tục, không restart
await route.fulfill({ response });
});
Bảng so sánh:
| Thuộc tính | Test retries (retries) |
route.fetch({ maxRetries }) |
|---|---|---|
| Phạm vi | Toàn bộ test function | Một network request cụ thể |
| Trigger | Bất kỳ assertion/error nào trong test | Network error khi gọi route.fetch() |
| Khi retry | Chạy lại từ đầu test | Thử lại request, test không restart |
| Side effect | Setup/teardown chạy lại | Chỉ gửi request thêm lần |
| Config location | playwright.config.ts |
Option của route.fetch() |
Hai cơ chế không thay thế nhau — chúng giải quyết hai vấn đề khác nhau. maxRetries giải quyết transient network error tại một request. Test retries giải quyết flaky test do bất kỳ nguyên nhân nào trong toàn test flow.
Pattern Resilient Fetch
Pattern phổ biến nhất: fetch response thật từ backend, modify nếu cần, rồi fulfill. Khi backend staging không ổn định, thêm maxRetries để tránh test fail do transient connection issue:
// Pattern cơ bản — resilient fetch + fulfill
await page.route('**/api/flaky', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
await route.fulfill({ response });
});
Pattern này: lấy response thật từ server (retry tối đa 3 lần nếu network error), sau đó forward nguyên response về browser mà không modify gì.
Kết hợp modify response
Trong nhiều trường hợp, fetch thật rồi modify một phần response body:
await page.route('**/api/user/profile', async (route) => {
// Fetch thật từ staging server (có thể intermittent)
const response = await route.fetch({ maxRetries: 3 });
// Modify response body để test với data cụ thể
const json = await response.json();
json.featureFlags = { ...json.featureFlags, newDashboard: true };
await route.fulfill({
response,
body: JSON.stringify(json),
contentType: 'application/json',
});
});
Xử lý khi tất cả retry đều fail
Nếu tất cả N+1 attempt đều fail với network error, route.fetch() ném lỗi JavaScript. Test sẽ fail với lỗi đó trừ khi có error handling trong route handler:
await page.route('**/api/critical', async (route) => {
try {
const response = await route.fetch({ maxRetries: 3 });
await route.fulfill({ response });
} catch (error) {
// Tất cả 4 attempt (1 initial + 3 retry) đều fail
// Fallback: fulfill với error response để test tiếp tục
await route.fulfill({
status: 503,
body: JSON.stringify({ error: 'Service unavailable' }),
contentType: 'application/json',
});
}
});
Có nên dùng try/catch ở đây hay không phụ thuộc vào mục tiêu test: nếu muốn test fail ngay khi backend không đạt được → để lỗi propagate. Nếu muốn test kiểm tra behavior của app khi API trả về 503 → dùng try/catch và fulfill với response giả.
Use Case Trong Test Environment
Staging backend không ổn định
Staging server thường có uptime thấp hơn production — restart thường xuyên, connection pool giới hạn, nhiều team dùng chung. Transient ECONNRESET hoặc ECONNREFUSED trong staging là bình thường và không phản ánh lỗi thật của app được test.
// Tất cả request đến staging API đều có retry
test.beforeEach(async ({ page }) => {
await page.route('https://staging-api.example.com/**', async (route) => {
const response = await route.fetch({ maxRetries: 2 });
await route.fulfill({ response });
});
});
CI network noise
CI runners (GitHub Actions, GitLab CI, Jenkins) chạy trong môi trường container hoặc VM có network overhead cao hơn máy local. Packet loss và connection reset thoáng qua (không nhất quán) hay xuất hiện hơn. maxRetries: 2 đủ để hấp thụ phần lớn transient CI noise mà không làm tăng latency đáng kể khi không có lỗi:
// Chỉ bật retry trong CI
const isCI = !!process.env.CI;
await page.route('**/api/**', async (route) => {
const response = await route.fetch({
maxRetries: isCI ? 2 : 0,
});
await route.fulfill({ response });
});
API endpoint với cold start
Serverless function (Lambda, Cloud Run) cold start đôi khi biểu hiện như ECONNREFUSED trong vài trăm millisecond đầu. maxRetries: 1 thường đủ để qua cold start mà không cần sleep trong test:
await page.route('**/api/serverless-fn', async (route) => {
const response = await route.fetch({ maxRetries: 1 });
await route.fulfill({ response });
});
Kết Hợp Với timeout
Bài 118 sẽ cover timeout trong route.fetch() chi tiết. Ở đây chỉ đề cập điểm giao nhau quan trọng khi dùng cả hai option cùng nhau.
Cú pháp kết hợp:
const response = await route.fetch({
maxRetries: 3,
timeout: 10_000,
});
Điểm cần lưu ý: timeout ở đây là timeout cho mỗi attempt riêng lẻ, không phải tổng thời gian của tất cả retry. Với maxRetries: 3, timeout: 10_000, tổng thời gian tối đa có thể là 4 × 10s = 40s (1 initial + 3 retry, mỗi attempt timeout sau 10s).
// Ví dụ timeline khi mọi attempt đều timeout:
// t=0: attempt 1 bắt đầu
// t=10s: attempt 1 timeout (ECONNRESET hoặc socket timeout)
// t=10s: attempt 2 bắt đầu (ngay lập tức)
// t=20s: attempt 2 timeout
// t=20s: attempt 3 bắt đầu
// t=30s: attempt 3 timeout
// t=30s: attempt 4 (final) bắt đầu
// t=40s: attempt 4 timeout → lỗi được ném ra
Hàm ý: nếu test timeout của Playwright là 30s và route handler dùng maxRetries: 3, timeout: 10_000, test timeout có thể hết trước khi tất cả retry hoàn thành. Test sẽ fail với "Test timeout exceeded" thay vì lỗi từ route.fetch(). Cần cân bằng maxRetries × timeout với test timeout tổng thể.
// Tính toán cẩn thận
// Test timeout: 60_000ms
// route.fetch timeout: 5_000ms per attempt
// maxRetries: 3 → tối đa 4 × 5_000 = 20_000ms cho fetch
// Còn 40_000ms cho phần còn lại của test
const response = await route.fetch({
maxRetries: 3,
timeout: 5_000, // 5s per attempt, không phải tổng
});
4 Pitfall Thực Tế
1. Expect retry 5xx nhưng không thấy retry
// MỤC TIÊU: muốn retry khi server trả 500
await page.route('**/api/data', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
// Server trả 500 → response.status() === 500
// maxRetries: 3 KHÔNG retry — 500 là valid HTTP response
// response nhận được bình thường, không exception
await route.fulfill({ response });
});
// FIX: nếu cần retry 5xx, implement thủ công
await page.route('**/api/data', async (route) => {
let response;
for (let i = 0; i <= 3; i++) {
response = await route.fetch();
if (response.status() < 500) break;
if (i === 3) break; // hết retry
}
await route.fulfill({ response });
});
2. Dùng maxRetries trên Playwright version < v1.46
// Playwright v1.45 hoặc cũ hơn
const response = await route.fetch({ maxRetries: 3 });
// Option maxRetries không được nhận diện → bị ignored
// route.fetch() vẫn chạy bình thường nhưng không có retry
// Không có warning, không có error
// Kiểm tra version trước khi dựa vào option này:
// npx playwright --version → 1.46.0 trở lên mới có
3. maxRetries cao kết hợp timeout thấp — vượt test timeout
// playwright.config.ts: timeout: 30_000
// NGUY HIỂM: 5 attempts × 8s = 40s > test timeout 30s
await page.route('**/api/slow', async (route) => {
const response = await route.fetch({
maxRetries: 4, // 1 initial + 4 retry = 5 attempts
timeout: 8_000, // 8s per attempt
});
// Test timeout (30s) hết ở attempt 4 → "Test timeout exceeded"
await route.fulfill({ response });
});
// FIX: đảm bảo (maxRetries + 1) × timeout < test timeout có buffer
// Hoặc tăng test timeout nếu retry là cần thiết
4. Nhầm maxRetries của route.fetch() với test retries
// Nhầm: nghĩ maxRetries sẽ giúp test không fail khi có flaky assertion
await page.route('**/api/data', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
await route.fulfill({ response });
});
test('check data', async ({ page }) => {
await page.goto('/dashboard');
// fetch được retry nếu ECONNRESET
// nhưng assertion dưới đây fail → test vẫn fail bình thường
await expect(page.getByText('Unexpected Content')).toBeVisible();
// maxRetries không giúp gì ở đây
});
// Để retry khi assertion flaky, dùng test-level retries:
// playwright.config.ts: retries: 2
Tổng Kết
route.fetch({ maxRetries: N })tự động retry request tối đa N lần khi gặp network error. Thêm từ Playwright v1.46, default là0.- Chỉ retry network error ở tầng connection (ECONNRESET, ECONNREFUSED, drop). HTTP error (4xx/5xx) là valid response — không retry.
- Khác hoàn toàn test-level
retries:maxRetrieschỉ retry một request cụ thể, không restart test. - Pattern resilient fetch:
route.fetch({ maxRetries }) → route.fulfill({ response })hữu dụng với staging backend không ổn định và CI network noise. - Khi kết hợp với
timeout, chú ý cumulative time:(maxRetries + 1) × timeoutphải nhỏ hơn test timeout còn lại. - v1.46+ only — trên version cũ, option bị ignored mà không có cảnh báo.
Quiz Củng Cố
Câu 1
Server trả về HTTP 500 Internal Server Error. Route handler có maxRetries: 3. Điều gì xảy ra?
await page.route('**/api/data', async (route) => {
const response = await route.fetch({ maxRetries: 3 });
await route.fulfill({ response });
});
Đáp án
route.fetch() nhận được response với status: 500 bình thường và trả về ngay — không retry. HTTP 500 là valid HTTP response về mặt network. maxRetries chỉ retry khi không nhận được HTTP response (network error ở tầng TCP/connection). route.fulfill({ response }) forward response 500 về browser.
Câu 2
Tính số attempt tối đa và thời gian tối đa trong tình huống xấu nhất:
const response = await route.fetch({
maxRetries: 4,
timeout: 6_000,
});
Đáp án
Số attempt tối đa: maxRetries + 1 = 5 (1 initial + 4 retry). Thời gian tối đa nếu tất cả đều timeout: 5 × 6_000 = 30_000ms = 30s. Nếu test timeout là 30s, test có thể bị kill bởi test timeout trước khi attempt cuối hoàn thành.
Câu 3
Đoạn code sau chạy trên Playwright v1.45. Behavior là gì?
const response = await route.fetch({ maxRetries: 3 });
Đáp án
Option maxRetries bị ignored vì v1.45 không hỗ trợ option này (được thêm từ v1.46). route.fetch() vẫn chạy bình thường nhưng không có retry — tức là nếu gặp ECONNRESET, lỗi được ném ra ngay lập tức. Không có cảnh báo hay lỗi compile-time nào chỉ ra rằng option bị ignored.
Câu 4
Phân biệt hai config sau — khi nào dùng cái nào?
// Config A — playwright.config.ts
export default defineConfig({ retries: 2 });
// Config B — trong route handler
await route.fetch({ maxRetries: 2 });
Đáp án
Config A (test-level retries): dùng khi test fail vì bất kỳ nguyên nhân nào (assertion fail, network lỗi, app bug bất thường). Toàn bộ test chạy lại từ đầu — setup, actions, teardown tất cả đều lặp lại.
Config B (route.fetch maxRetries): dùng khi muốn tăng độ bền của một network request cụ thể trước transient connection error, mà không muốn restart toàn bộ test. Test tiếp tục ngay sau khi fetch thành công (hoặc ném lỗi nếu hết retry).
Hai cơ chế bổ sung cho nhau chứ không thay thế nhau.
Bài Tiếp Theo
Bài 118 tiếp tục nhóm Routing nâng cao với option timeout của route.fetch() — cách giới hạn thời gian chờ cho từng fetch request trong route handler, khác với navigationTimeout và actionTimeout trong config.
