Danh sách bài viết

Bài 17: Option Fixture extraHTTPHeaders, httpCredentials

extraHTTPHeaders gắn một tập header cố định vào mọi HTTP request phát ra từ BrowserContext — bao gồm cả navigation, AJAX, và request fixture. httpCredentials cung cấp username/password để browser tự động đáp ứng HTTP Basic Auth challenge (RFC 7617). Từ v1.45, httpCredentials bổ sung field origin để scope credentials cho một origin cụ thể, và send: 'always' để gửi header Authorization ngay từ request đầu mà không cần chờ server trả 401. Bài trình bày: cơ chế của cả hai option, use cases thực tế, override runtime với setExtraHTTPHeaders(), per-test override qua test.use, phân biệt với cookie-based auth và per-call headers, cách xử lý sensitive value an toàn, limitations, 4 pitfall hay gặp, và quiz 5 câu.

27/05/2026
15 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu extraHTTPHeaders hoạt động ở context level — mọi request trong context đều nhận headers này, không cần khai báo lại từng request.
  • Hiểu httpCredentials xử lý HTTP Basic Auth (RFC 7617) tự động — không cần viết code xử lý redirect 401.
  • Biết các field originsend của httpCredentials được thêm từ v1.45 và dùng khi nào.
  • Biết cách override runtime với context.setExtraHTTPHeaders() và hệ quả replace-not-merge.
  • Phân biệt extraHTTPHeaders với per-call headers và cookie-based authentication.
  • Áp dụng pattern env var để tránh hardcode credential trong codebase.
  • Tránh 4 pitfall phổ biến khi sử dụng hai option này.
2

extraHTTPHeaders — Cơ Chế

extraHTTPHeaders là một object kiểu Record<string, string> — map từ tên header sang giá trị header. Khi được set ở use: { ... }, Playwright truyền object này vào BrowserContext khi khởi tạo.

Khai báo trong playwright.config.ts:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    extraHTTPHeaders: {
      'Authorization': `Bearer ${process.env.TEST_TOKEN}`,
      'X-Test-Env': 'e2e',
    },
  },
});

Sau khi set, mọi HTTP request phát sinh từ context đó đều tự động kèm theo các headers này:

  • Navigation: page.goto(), click link, submit form
  • AJAX / Fetch từ JavaScript trong trang
  • Subresource (script, stylesheet, image) cùng origin
  • request fixture (APIRequestContext) — cũng dùng chung context headers

Headers được merge vào request headers tồn tại, không thay thế hoàn toàn. Nếu request đã có header cùng tên, giá trị từ extraHTTPHeaders sẽ ghi đè (override) header đó cho request cụ thể đó.

Tên header trong HTTP là case-insensitive (RFC 7230). Playwright normalize về lowercase trước khi gửi. Trong config nên giữ nhất quán — phổ biến nhất là dùng mixed-case như 'Authorization', 'X-Test-Env'.

3

Use Cases Của extraHTTPHeaders

API Auth Token

Khi backend yêu cầu Authorization: Bearer <token> cho mọi endpoint, extraHTTPHeaders thay thế việc phải đăng nhập qua UI hoặc setup OAuth flow trong test:

use: {
  extraHTTPHeaders: {
    'Authorization': `Bearer ${process.env.TEST_TOKEN}`,
  },
}

Test truy cập thẳng endpoint được bảo vệ mà không cần qua luồng login. Đây là pattern phổ biến với SPA + API backend tách rời.

Test Environment Flag

Backend E2E environment có thể đọc header để bypass rate limiting, disable CAPTCHA, hoặc kích hoạt test-only behavior:

use: {
  extraHTTPHeaders: {
    'X-E2E': 'true',
    'X-Test-Env': 'playwright',
  },
}

Pattern này giữ logic E2E trong backend code riêng biệt. Test không cần mock hay workaround phức tạp.

A/B Test Flag

Force variant cụ thể cho A/B test thay vì để server random:

// Trong describe block test variant B
test.use({
  extraHTTPHeaders: {
    'X-AB-Variant': 'B',
  },
});

Tracing và Log Correlation

Gắn request ID để tương quan log giữa test runner và backend:

test('checkout flow', async ({ page }, testInfo) => {
  // testInfo.testId là unique string cho mỗi test
  await page.context().setExtraHTTPHeaders({
    'X-Request-ID': `test-${testInfo.testId}`,
  });
  // Mọi request từ đây đều có X-Request-ID để trace trong log backend
  await page.goto('/checkout');
});
4

httpCredentials — HTTP Basic Auth

HTTP Basic Authentication (RFC 7617) là cơ chế xác thực ở tầng HTTP: server trả về 401 Unauthorized kèm header WWW-Authenticate: Basic realm="...", browser hiện dialog yêu cầu nhập username/password, sau đó gửi lại request với header Authorization: Basic <base64(user:pass)>.

Trong browser tự động (không có UI interaction), dialog này không thể tương tác được. httpCredentials cung cấp credentials để Playwright tự động điền khi gặp challenge này:

// playwright.config.ts
export default defineConfig({
  use: {
    httpCredentials: {
      username: 'admin',
      password: 'secret',
    },
  },
});

Khi context gặp response 401 với WWW-Authenticate: Basic, nó tự động resend request kèm Authorization: Basic YWRtaW46c2VjcmV0 (base64 của admin:secret). Test không cần xử lý dialog hay redirect thủ công.

Lưu ý quan trọng: httpCredentials chỉ áp dụng cho HTTP Basic Auth. Không áp dụng cho:

  • Form-based login (HTML form với username/password input)
  • OAuth / OIDC flow
  • Digest Auth (khác cơ chế với Basic Auth)
  • NTLM / Kerberos (Windows authentication)
5

Field originsend [v1.45+]

Playwright v1.45 bổ sung hai field tùy chọn cho httpCredentials:

Field origin — Giới Hạn Credentials Theo Origin

Mặc định, credentials được gửi cho mọi origin trong context khi gặp Basic Auth challenge. Field origin cho phép scope credentials chỉ cho một origin cụ thể:

use: {
  httpCredentials: {
    username: 'admin',
    password: 'secret',
    origin: 'https://protected.example.com',
  },
}

Với config trên, credentials chỉ được gửi khi challenge đến từ https://protected.example.com. Challenge từ các origin khác (ví dụ third-party resource) không được tự động đáp ứng.

Field send — Kiểm Soát Thời Điểm Gửi

Field send có hai giá trị:

  • 'unauthorized' (default): credentials chỉ được gửi sau khi server trả 401. Đây là flow chuẩn của HTTP Basic Auth.
  • 'always': gửi header Authorization ngay từ request đầu tiên, không cần chờ challenge 401. Hữu ích khi server không gửi 401 challenge (ví dụ server trả 404 thay vì 401 cho unauthenticated request).
use: {
  httpCredentials: {
    username: 'admin',
    password: 'secret',
    origin: 'https://protected.example.com',
    send: 'always',  // Gửi ngay, không chờ 401
  },
}

Kết hợp originsend: 'always' là pattern phổ biến nhất trong test môi trường staging có Basic Auth bảo vệ toàn bộ site.

6

Override Runtime Với setExtraHTTPHeaders()

Trong quá trình chạy test, có thể thay đổi extraHTTPHeaders của context bằng phương thức context.setExtraHTTPHeaders():

test('switch token mid-test', async ({ context, page }) => {
  // Bắt đầu với token A từ config
  await page.goto('/dashboard');

  // Đổi sang token B cho phần còn lại của test
  await context.setExtraHTTPHeaders({
    'Authorization': 'Bearer token-B',
    'X-Test-Env': 'e2e',  // Phải khai báo lại
  });

  await page.goto('/admin');
});

Hành vi quan trọng: setExtraHTTPHeaders() REPLACE toàn bộ headers hiện tại, không merge.

Sau khi gọi setExtraHTTPHeaders({ 'Authorization': 'Bearer token-B' }), header 'X-Test-Env': 'e2e' từ config ban đầu bị xóa nếu không được khai báo lại. Đây là nguồn gốc của pitfall phổ biến nhất khi dùng API này (xem mục 12).

Pattern an toàn — spread để giữ headers cũ và chỉ override headers cần thay:

// Giả sử đã có originalHeaders từ config hoặc biến lưu trước
const originalHeaders = {
  'X-Test-Env': 'e2e',
};

test('safe override', async ({ context, page }) => {
  await context.setExtraHTTPHeaders({
    ...originalHeaders,
    'Authorization': 'Bearer new-token',  // Override hoặc thêm mới
  });
});
7

Per-Test Override Qua test.use

Cũng như các Option Fixture khác, extraHTTPHeadershttpCredentials có thể override ở file hoặc describe scope qua test.use():

Override Tại File Level

// admin-api.spec.ts
import { test, expect } from '@playwright/test';

// Tất cả test trong file này dùng admin token
test.use({
  extraHTTPHeaders: {
    'Authorization': `Bearer ${process.env.ADMIN_TOKEN}`,
    'X-Test-Env': 'e2e',
  },
});

test('list users', async ({ request }) => {
  const res = await request.get('/api/admin/users');
  expect(res.ok()).toBeTruthy();
});

test('delete user', async ({ request }) => {
  const res = await request.delete('/api/admin/users/123');
  expect(res.status()).toBe(200);
});

Override Tại Describe Level

// role-based.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Admin role', () => {
  test.use({
    extraHTTPHeaders: {
      'Authorization': `Bearer ${process.env.ADMIN_TOKEN}`,
    },
  });

  test('can access admin panel', async ({ page }) => {
    await page.goto('/admin');
    await expect(page.locator('h1')).toContainText('Admin Panel');
  });
});

test.describe('Regular user role', () => {
  test.use({
    extraHTTPHeaders: {
      'Authorization': `Bearer ${process.env.USER_TOKEN}`,
    },
  });

  test('cannot access admin panel', async ({ page }) => {
    await page.goto('/admin');
    await expect(page).toHaveURL('/403');
  });
});

Khi dùng test.use({ extraHTTPHeaders: {...} }), giá trị này replace hoàn toàn extraHTTPHeaders từ project config — không merge. Cần khai báo lại tất cả headers cần thiết trong object mới nếu muốn giữ chúng.

8

Dùng Với request Fixture

request fixture trong Playwright là APIRequestContext — không đi qua browser, nhưng vẫn nhận extraHTTPHeaders từ context config vì nó được khởi tạo từ cùng fixture scope.

Kịch bản thực tế: test một API endpoint yêu cầu Authorization header:

// playwright.config.ts
export default defineConfig({
  use: {
    baseURL: 'https://api.staging.example.com',
    extraHTTPHeaders: {
      'Authorization': `Bearer ${process.env.TEST_TOKEN}`,
    },
  },
});

// api-tests.spec.ts
test('protected endpoint trả 200', async ({ request }) => {
  // Header 'Authorization' được tự động đính kèm
  // Không cần khai báo headers trong từng request.get()
  const res = await request.get('/api/admin/users');
  expect(res.ok()).toBeTruthy();
  const data = await res.json();
  expect(Array.isArray(data)).toBe(true);
});

test('tạo resource mới', async ({ request }) => {
  const res = await request.post('/api/admin/resources', {
    data: { name: 'test-resource' },
    // Authorization header tự động có, không cần thêm vào đây
  });
  expect(res.status()).toBe(201);
});

Pattern này đặc biệt hữu ích khi viết API test suite: config extraHTTPHeaders một lần ở project/file level, tất cả request.get/post/put/delete đều mang token mà không cần lặp lại.

Nếu một request cụ thể cần override header (ví dụ test với token hết hạn), vẫn có thể truyền headers trực tiếp:

test('expired token trả 401', async ({ request }) => {
  const res = await request.get('/api/admin/users', {
    headers: {
      // Override Authorization cho chỉ request này
      'Authorization': 'Bearer expired-token',
    },
  });
  expect(res.status()).toBe(401);
});
9

Phân Biệt Với Cookie Auth Và Per-Call Headers

extraHTTPHeaders Vs Cookie-Based Auth

Tiêu chí Cookie Auth (storageState) extraHTTPHeaders
Nguồn gốc Server-set (Set-Cookie) Client-side, dev tự định nghĩa
Cơ chế Browser tự đính kèm cookie theo domain Playwright đính kèm header vào mọi request
Phù hợp khi App dùng session cookie (form login) App dùng token-based API (JWT, API key)
Setup Cần đăng nhập trước, lưu state Chỉ cần set token trong config
Cross-origin Bị giới hạn bởi SameSite policy Áp dụng cho mọi origin (trừ cross-origin iframe)

storageState phù hợp khi app dùng session-based auth qua form login — đã được đề cập chi tiết ở bài 12 của module này và Series Cơ Bản. extraHTTPHeaders phù hợp khi test API trực tiếp với token tĩnh.

extraHTTPHeaders Vs Per-Call Headers

Tiêu chí extraHTTPHeaders Per-call headers
Phạm vi Mọi request trong context Chỉ request đó
Cách dùng use: { extraHTTPHeaders } trong config request.get('/path', { headers: {...} })
Phù hợp khi Auth token cần có ở mọi nơi Header đặc biệt chỉ cho 1 call cụ thể
Maintenance Set 1 lần, áp dụng khắp nơi Phải nhớ thêm vào mỗi request
10

Sensitive Value — Dùng Env Var

Token, password, API key là sensitive value — không được hardcode trong file config đưa lên git.

Pattern Env Var

// playwright.config.ts
export default defineConfig({
  use: {
    extraHTTPHeaders: {
      // Đọc từ biến môi trường — undefined nếu không set
      'Authorization': `Bearer ${process.env.TEST_TOKEN ?? ''}`,
      'X-Test-Env': 'e2e',
    },
    httpCredentials: {
      username: process.env.BASIC_AUTH_USER ?? '',
      password: process.env.BASIC_AUTH_PASS ?? '',
    },
  },
});

File .env Với dotenv

Playwright Test từ v1.40+ tự động load file .env trong project root (không cần cài thêm gói dotenv):

# .env  — KHÔNG commit file này lên git
TEST_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
BASIC_AUTH_USER=admin
BASIC_AUTH_PASS=s3cr3t!
# .gitignore
.env
.env.local
playwright/.auth/

CI/CD

Trong CI, inject env var qua secrets của pipeline — không đặt trong file:

# GitHub Actions
- name: Run Playwright tests
  env:
    TEST_TOKEN: ${{ secrets.PLAYWRIGHT_TEST_TOKEN }}
    BASIC_AUTH_USER: ${{ secrets.STAGING_USER }}
    BASIC_AUTH_PASS: ${{ secrets.STAGING_PASS }}
  run: npx playwright test

Pattern này đảm bảo: local dev dùng file .env (không commit), CI dùng secrets, không bao giờ có token trong source code.

11

Limitations

Một số giới hạn cần biết trước khi sử dụng:

extraHTTPHeaders Không Áp Dụng Cho Cross-Origin Subresource Trong Iframe

Nếu trang load iframe từ một origin khác, subresource request bên trong iframe đó (script, fetch, XHR) không nhận extraHTTPHeaders. Đây là giới hạn của kiến trúc Playwright liên quan đến process isolation cho cross-origin frame.

Request navigation đến chính iframe URL vẫn nhận headers. Chỉ các request bên trong cross-origin iframe mới không nhận.

httpCredentials Chỉ Cho HTTP Basic Auth

Như đã đề cập ở mục 4: httpCredentials không hoạt động với form-based login, OAuth, Digest Auth, hay NTLM. Nhầm lẫn điểm này là pitfall thường gặp nhất.

Header Case-Insensitive Nhưng Cần Nhất Quán

HTTP spec quy định tên header là case-insensitive. Tuy nhiên, một số middleware hoặc proxy có behavior khác nhau với casing. Nên giữ convention nhất quán trong toàn project — phổ biến nhất là title-case ('Authorization', 'Content-Type').

setExtraHTTPHeaders() Là Synchronous Replace

Phương thức này replace toàn bộ headers trong một lần gọi. Không có API addExtraHTTPHeader() để thêm một header mà giữ nguyên các header còn lại.

12

4 Pitfall Thực Tế

Pitfall 1 — Hardcode Token Trong Config

// SAI — token bị lộ trong git history
extraHTTPHeaders: {
  'Authorization': 'Bearer eyJhbGci...actual-secret-token...',
}
// ĐÚNG — đọc từ env var
extraHTTPHeaders: {
  'Authorization': `Bearer ${process.env.TEST_TOKEN ?? ''}`,
}

Một khi token đã commit lên git, dù xóa đi sau đó, git history vẫn còn. Cần rotate token ngay nếu xảy ra.

Pitfall 2 — setExtraHTTPHeaders() Xóa Hết Headers Cũ

// Config ban đầu:
// extraHTTPHeaders: { 'Authorization': 'Bearer A', 'X-Test-Env': 'e2e' }

test('pitfall', async ({ context }) => {
  // SAI — X-Test-Env bị mất sau lệnh này
  await context.setExtraHTTPHeaders({
    'Authorization': 'Bearer B',
  });

  // ĐÚNG — khai báo lại đầy đủ
  await context.setExtraHTTPHeaders({
    'Authorization': 'Bearer B',
    'X-Test-Env': 'e2e',  // Phải nhớ giữ lại
  });
});

Pitfall 3 — Nhầm httpCredentials Với Form Login

// httpCredentials KHÔNG giải quyết được form login
// SAI assumption: nghĩ set httpCredentials = không cần login qua UI
httpCredentials: {
  username: '[email protected]',
  password: 'mypassword',
}
// Nếu app dùng HTML form login, test vẫn thấy login page
// và không tự động submit form

// ĐÚNG: với form login, dùng storageState (bài 12)
// hoặc login flow trong beforeAll/setup

Pitfall 4 — Token Hết Hạn Giữa Test Suite

Khi test suite chạy dài (nhiều giờ trong CI), JWT token ngắn hạn có thể hết hạn giữa chừng. Request trả 401 dù config đúng, khiến test fail không rõ nguyên nhân.

Cách xử lý:

  • Dùng long-lived test token được cấp riêng cho CI — không dùng token user thật.
  • Hoặc generate token mới trước mỗi test run trong global setup.
  • Monitor 401 trong test failures — nếu thấy pattern token expiry, thêm log để phân biệt với lỗi thật.
// global-setup.ts — generate fresh token trước mỗi run
import { request } from '@playwright/test';

async function globalSetup() {
  const req = await request.newContext();
  const res = await req.post('https://auth.example.com/token', {
    data: {
      grant_type: 'client_credentials',
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
    },
  });
  const { access_token } = await res.json();
  // Ghi ra env var để playwright.config.ts đọc
  process.env.TEST_TOKEN = access_token;
}

export default globalSetup;
13

Quiz

Câu 1: extraHTTPHeaders được set trong playwright.config.ts. Khi test dùng request fixture để gọi request.get('/api/data'), header đó có được đính kèm không?

Đáp án: Có. request fixture là APIRequestContext được khởi tạo từ cùng fixture scope với pagecontext. Nó nhận extraHTTPHeaders từ config và đính kèm vào mọi request, bao gồm cả request.get().

Câu 2: Config có extraHTTPHeaders: { 'Authorization': 'Bearer A', 'X-E2E': 'true' }. Trong test gọi await context.setExtraHTTPHeaders({ 'Authorization': 'Bearer B' }). Sau lệnh này, request sẽ có những headers nào?

Đáp án: Chỉ có Authorization: Bearer B. setExtraHTTPHeaders() replace toàn bộ headers cũ, không merge. Header X-E2E: true bị xóa. Cần khai báo lại: { 'Authorization': 'Bearer B', 'X-E2E': 'true' }.

Câu 3: App dùng form login (HTML username/password form). Tôi set httpCredentials: { username: 'admin', password: 'pass' } trong config. Test có tự động đăng nhập không?

Đáp án: Không. httpCredentials chỉ xử lý HTTP Basic Auth (RFC 7617) — khi server trả 401 với header WWW-Authenticate: Basic. Với form-based login, cần tương tác UI (fill + click submit) hoặc dùng storageState sau khi đã đăng nhập một lần.

Câu 4: Sự khác biệt giữa httpCredentials.send: 'unauthorized'send: 'always' là gì? Khi nào nên dùng 'always'?

Đáp án: 'unauthorized' (default) — credentials chỉ gửi sau khi nhận được 401 challenge từ server. 'always' — gửi header Authorization ngay từ request đầu tiên mà không chờ challenge. Dùng 'always' khi server không trả 401 challenge đúng chuẩn (ví dụ redirect thẳng về login page, hoặc trả 404), hoặc khi muốn tránh round-trip 401 → resend để tăng tốc test.

Câu 5: File describe-a.spec.tstest.use({ extraHTTPHeaders: { 'X-Role': 'admin' } }). Sau đó project config có extraHTTPHeaders: { 'Authorization': 'Bearer token', 'X-E2E': 'true' }. Khi test trong file này chạy, nó có Authorization header không?

Đáp án: Không. test.use({ extraHTTPHeaders: {...} }) ở file level replace hoàn toàn giá trị từ project config, không merge. Test chỉ có X-Role: admin. Để giữ AuthorizationX-E2E, phải khai báo lại đầy đủ trong test.use: { 'Authorization': 'Bearer token', 'X-E2E': 'true', 'X-Role': 'admin' }.

14

Bài Tiếp Theo

Bài 18 tiếp tục nhóm Options Fixtures với proxyignoreHTTPSErrors — hai option kiểm soát cách context xử lý network routing và SSL certificate trong môi trường test.

Bài 18: Option Fixture proxy, ignoreHTTPSErrors