← Trở về trang chủ

HTTP Methods (Phương thức HTTP)

HTTP Method là gì?

HTTP Methods (Phương thức HTTP) là các động từ (verbs) được sử dụng để chỉ định hành động mong muốn được thực hiện trên một tài nguyên (resource) trên server.

Điểm quan trọng:
  • Mỗi HTTP request phải có một method
  • Method xác định ý định của request
  • RESTful API sử dụng HTTP methods để thực hiện các thao tác CRUD
  • Một số methods là idempotent (gọi nhiều lần cho kết quả giống nhau)
  • Một số methods là safe (chỉ đọc, không thay đổi dữ liệu)

Các HTTP Methods phổ biến

Method Mô tả Safe Idempotent CRUD
GET Lấy dữ liệu từ server Read
POST Tạo mới tài nguyên Create
PUT Cập nhật toàn bộ tài nguyên Update
PATCH Cập nhật một phần tài nguyên Update
DELETE Xóa tài nguyên Delete
HEAD Giống GET nhưng không trả về body -
OPTIONS Kiểm tra các methods được hỗ trợ -
⚠️ Lưu ý:
  • Safe methods: Không làm thay đổi dữ liệu trên server (GET, HEAD, OPTIONS)
  • Idempotent methods: Gọi nhiều lần cho cùng một kết quả (GET, PUT, DELETE, HEAD, OPTIONS)
  • Non-idempotent: POST và PATCH có thể cho kết quả khác nhau mỗi lần gọi

GET Phương thức GET

Mục đích: Lấy dữ liệu từ server mà không làm thay đổi dữ liệu.

Đặc điểm:

  • ✓ Safe: Không làm thay đổi trạng thái server
  • ✓ Idempotent: Gọi nhiều lần cho cùng một kết quả
  • ✓ Có thể được cache
  • ✓ Có thể được bookmark
  • ✓ Có thể được lưu trong browser history
  • ✗ Không nên gửi dữ liệu nhạy cảm qua URL
  • ✗ Có giới hạn độ dài URL (~2048 ký tự)
Khi nào sử dụng GET?
  • Lấy danh sách tài nguyên (ví dụ: danh sách sản phẩm)
  • Lấy chi tiết một tài nguyên (ví dụ: thông tin user)
  • Tìm kiếm và lọc dữ liệu
  • Bất kỳ thao tác chỉ đọc nào
⚠️ Không nên dùng GET cho:
  • Gửi mật khẩu hoặc dữ liệu nhạy cảm
  • Thay đổi dữ liệu trên server
  • Gửi dữ liệu lớn (dùng POST thay thế)

Ví dụ 1: Lấy danh sách users

GET /api/users HTTP/1.1
Host: example.com
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    "id": 1,
    "name": "Nguyễn Văn A",
    "email": "vana@example.com"
  },
  {
    "id": 2,
    "name": "Trần Thị B",
    "email": "thib@example.com"
  }
]

Ví dụ 2: Lấy chi tiết một user

GET /api/users/1 HTTP/1.1
Host: example.com
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,
  "name": "Nguyễn Văn A",
  "email": "vana@example.com",
  "age": 25,
  "city": "Hà Nội"
}

Ví dụ 3: Tìm kiếm với query parameters

GET /api/products?category=laptop&price_max=20000000&sort=price_asc HTTP/1.1
Host: example.com
Accept: application/json

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "total": 15,
  "page": 1,
  "data": [
    {
      "id": 101,
      "name": "Laptop Dell Inspiron",
      "price": 15000000,
      "category": "laptop"
    }
  ]
}

Ví dụ 4: Sử dụng Playwright để test GET request

// Test với Playwright
import { test, expect } from '@playwright/test';

test('GET - Lấy danh sách users', async ({ request }) => {
  const response = await request.get('https://example.com/api/users');

  expect(response.status()).toBe(200);

  const users = await response.json();
  expect(users).toBeInstanceOf(Array);
  expect(users.length).toBeGreaterThan(0);
});

test('GET - Lấy user theo ID', async ({ request }) => {
  const response = await request.get('https://example.com/api/users/1');

  expect(response.status()).toBe(200);

  const user = await response.json();
  expect(user).toHaveProperty('id', 1);
  expect(user).toHaveProperty('name');
});

POST Phương thức POST

Mục đích: Tạo mới tài nguyên hoặc gửi dữ liệu để xử lý trên server.

Đặc điểm:

  • ✗ Không safe: Có thể thay đổi trạng thái server
  • ✗ Không idempotent: Gọi nhiều lần có thể tạo nhiều tài nguyên
  • ✓ Gửi dữ liệu trong request body
  • ✓ Không có giới hạn kích thước dữ liệu
  • ✓ An toàn hơn GET khi gửi dữ liệu nhạy cảm
  • ✗ Không được cache mặc định
  • ✗ Không thể bookmark
Khi nào sử dụng POST?
  • Tạo mới tài nguyên (user mới, sản phẩm mới, ...)
  • Gửi form data
  • Upload file
  • Đăng nhập, đăng ký
  • Xử lý dữ liệu phức tạp
⚠️ Lưu ý:
  • Thường trả về status code 201 (Created) khi tạo thành công
  • Nên trả về thông tin của tài nguyên vừa tạo trong response
  • Nên include header Location với URL của tài nguyên mới

Ví dụ 1: Tạo user mới

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "name": "Lê Văn C",
  "email": "vanc@example.com",
  "password": "securePassword123",
  "age": 28
}

Response:

HTTP/1.1 201 Created
Location: /api/users/3
Content-Type: application/json

{
  "id": 3,
  "name": "Lê Văn C",
  "email": "vanc@example.com",
  "age": 28,
  "created_at": "2025-01-15T10:30:00Z"
}

Ví dụ 2: Đăng nhập

POST /api/auth/login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "email": "vana@example.com",
  "password": "myPassword123"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": 1,
    "name": "Nguyễn Văn A",
    "email": "vana@example.com"
  }
}

Ví dụ 3: Upload file

POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Type: image/jpeg

[binary data]
------WebKitFormBoundary--

Response:

HTTP/1.1 201 Created
Content-Type: application/json

{
  "file_id": "abc123",
  "url": "https://cdn.example.com/uploads/avatar.jpg",
  "size": 102400
}

Ví dụ 4: Test POST với Playwright

import { test, expect } from '@playwright/test';

test('POST - Tạo user mới', async ({ request }) => {
  const response = await request.post('https://example.com/api/users', {
    data: {
      name: 'Phạm Thị D',
      email: 'thid@example.com',
      password: 'password123'
    }
  });

  expect(response.status()).toBe(201);

  const user = await response.json();
  expect(user).toHaveProperty('id');
  expect(user.name).toBe('Phạm Thị D');
  expect(user.email).toBe('thid@example.com');
});

test('POST - Đăng nhập', async ({ request }) => {
  const response = await request.post('https://example.com/api/auth/login', {
    data: {
      email: 'vana@example.com',
      password: 'myPassword123'
    }
  });

  expect(response.status()).toBe(200);

  const result = await response.json();
  expect(result).toHaveProperty('token');
  expect(result.user).toHaveProperty('email', 'vana@example.com');
});

PUT Phương thức PUT

Mục đích: Cập nhật toàn bộ tài nguyên hoặc tạo mới nếu không tồn tại.

Đặc điểm:

  • ✗ Không safe: Thay đổi trạng thái server
  • ✓ Idempotent: Gọi nhiều lần cho cùng kết quả
  • ✓ Thay thế hoàn toàn tài nguyên
  • ✓ Cần gửi toàn bộ dữ liệu của tài nguyên
  • ✓ Có thể tạo mới nếu tài nguyên không tồn tại
PUT vs POST:
  • PUT: Idempotent, update toàn bộ tài nguyên, URL chỉ định tài nguyên cụ thể
  • POST: Không idempotent, tạo mới, URL chỉ định collection
Khi nào sử dụng PUT?
  • Cập nhật toàn bộ thông tin user
  • Thay thế hoàn toàn một tài nguyên
  • Khi biết chính xác ID/URL của tài nguyên
⚠️ Lưu ý:
  • PUT yêu cầu gửi toàn bộ dữ liệu, các field không gửi sẽ bị xóa/reset
  • Nếu chỉ muốn cập nhật một số field, dùng PATCH thay vì PUT
  • Trả về 200 (OK) hoặc 204 (No Content) khi cập nhật thành công
  • Trả về 201 (Created) nếu tạo mới tài nguyên

Ví dụ 1: Cập nhật toàn bộ thông tin user

PUT /api/users/1 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "name": "Nguyễn Văn A - Updated",
  "email": "vana_new@example.com",
  "age": 26,
  "city": "TP.HCM",
  "phone": "0123456789"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,
  "name": "Nguyễn Văn A - Updated",
  "email": "vana_new@example.com",
  "age": 26,
  "city": "TP.HCM",
  "phone": "0123456789",
  "updated_at": "2025-01-15T11:00:00Z"
}

Ví dụ 2: PUT có thể tạo mới nếu không tồn tại

PUT /api/users/999 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "name": "User Mới",
  "email": "usermoi@example.com",
  "age": 30
}

Response (nếu user 999 không tồn tại):

HTTP/1.1 201 Created
Location: /api/users/999
Content-Type: application/json

{
  "id": 999,
  "name": "User Mới",
  "email": "usermoi@example.com",
  "age": 30,
  "created_at": "2025-01-15T11:15:00Z"
}

Ví dụ 3: So sánh PUT với PATCH

Dữ liệu ban đầu:

{
  "id": 1,
  "name": "Nguyễn Văn A",
  "email": "vana@example.com",
  "age": 25,
  "city": "Hà Nội"
}

Sử dụng PUT (chỉ gửi name và email):

PUT /api/users/1
{
  "name": "Nguyễn Văn A Updated",
  "email": "vana_new@example.com"
}

// Kết quả: age và city bị mất!
{
  "id": 1,
  "name": "Nguyễn Văn A Updated",
  "email": "vana_new@example.com"
}

Sử dụng PATCH (chỉ gửi name và email):

PATCH /api/users/1
{
  "name": "Nguyễn Văn A Updated",
  "email": "vana_new@example.com"
}

// Kết quả: age và city vẫn còn!
{
  "id": 1,
  "name": "Nguyễn Văn A Updated",
  "email": "vana_new@example.com",
  "age": 25,
  "city": "Hà Nội"
}

Ví dụ 4: Test PUT với Playwright

import { test, expect } from '@playwright/test';

test('PUT - Cập nhật user', async ({ request }) => {
  const response = await request.put('https://example.com/api/users/1', {
    data: {
      name: 'Tên mới',
      email: 'email_moi@example.com',
      age: 30,
      city: 'Đà Nẵng'
    }
  });

  expect(response.status()).toBe(200);

  const user = await response.json();
  expect(user.name).toBe('Tên mới');
  expect(user.email).toBe('email_moi@example.com');
  expect(user.age).toBe(30);
});

test('PUT - Kiểm tra tính idempotent', async ({ request }) => {
  const requestData = {
    name: 'Test User',
    email: 'test@example.com',
    age: 25
  };

  // Gọi lần 1
  const response1 = await request.put('https://example.com/api/users/100', {
    data: requestData
  });
  const data1 = await response1.json();

  // Gọi lần 2 với cùng dữ liệu
  const response2 = await request.put('https://example.com/api/users/100', {
    data: requestData
  });
  const data2 = await response2.json();

  // Kết quả phải giống nhau
  expect(data1).toEqual(data2);
});

PATCH Phương thức PATCH

Mục đích: Cập nhật một phần (partial update) tài nguyên.

Đặc điểm:

  • ✗ Không safe: Thay đổi trạng thái server
  • ⚠️ Không nhất định idempotent (tùy implementation)
  • ✓ Chỉ gửi các field cần cập nhật
  • ✓ Các field không gửi sẽ giữ nguyên giá trị cũ
  • ✓ Hiệu quả hơn PUT khi chỉ cần cập nhật vài field
PATCH vs PUT:
Đặc điểm PUT PATCH
Cập nhật Toàn bộ tài nguyên Một phần tài nguyên
Dữ liệu gửi Tất cả các fields Chỉ fields cần update
Idempotent Tùy implementation
Fields không gửi Bị xóa/reset Giữ nguyên
Khi nào sử dụng PATCH?
  • Cập nhật một vài field của user (ví dụ: chỉ đổi email)
  • Cập nhật status của đơn hàng
  • Kích hoạt/vô hiệu hóa tài khoản
  • Bất kỳ thao tác cập nhật một phần nào
⚠️ Lưu ý:
  • PATCH thường trả về 200 (OK) kèm dữ liệu đã cập nhật
  • Có thể trả về 204 (No Content) nếu không trả về body
  • PATCH không phải lúc nào cũng idempotent (ví dụ: tăng counter)

Ví dụ 1: Cập nhật chỉ email

PATCH /api/users/1 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "email": "newemail@example.com"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1,
  "name": "Nguyễn Văn A",
  "email": "newemail@example.com",
  "age": 25,
  "city": "Hà Nội",
  "updated_at": "2025-01-15T12:00:00Z"
}

⚠️ Chú ý: Chỉ email thay đổi, các field khác giữ nguyên!

Ví dụ 2: Cập nhật status đơn hàng

PATCH /api/orders/12345 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "status": "shipped",
  "tracking_number": "VN123456789"
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 12345,
  "customer_name": "Nguyễn Văn A",
  "items": [...],
  "total": 5000000,
  "status": "shipped",
  "tracking_number": "VN123456789",
  "updated_at": "2025-01-15T12:15:00Z"
}

Ví dụ 3: PATCH không idempotent

PATCH /api/products/100/views HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "increment": 1
}

Response lần 1:

HTTP/1.1 200 OK

{
  "id": 100,
  "views": 101
}

Response lần 2 (gọi lại cùng request):

HTTP/1.1 200 OK

{
  "id": 100,
  "views": 102  // Khác với lần 1!
}

⚠️ Ví dụ này KHÔNG idempotent vì mỗi lần gọi sẽ tăng counter!

Ví dụ 4: Test PATCH với Playwright

import { test, expect } from '@playwright/test';

test('PATCH - Cập nhật email user', async ({ request }) => {
  // Lấy dữ liệu user hiện tại
  const getUserResponse = await request.get('https://example.com/api/users/1');
  const originalUser = await getUserResponse.json();

  // Cập nhật chỉ email
  const patchResponse = await request.patch('https://example.com/api/users/1', {
    data: {
      email: 'updated_email@example.com'
    }
  });

  expect(patchResponse.status()).toBe(200);

  const updatedUser = await patchResponse.json();

  // Email đã thay đổi
  expect(updatedUser.email).toBe('updated_email@example.com');

  // Các field khác không đổi
  expect(updatedUser.name).toBe(originalUser.name);
  expect(updatedUser.age).toBe(originalUser.age);
});

test('PATCH - Cập nhật nhiều fields', async ({ request }) => {
  const response = await request.patch('https://example.com/api/users/1', {
    data: {
      age: 30,
      city: 'Cần Thơ'
    }
  });

  expect(response.status()).toBe(200);

  const user = await response.json();
  expect(user.age).toBe(30);
  expect(user.city).toBe('Cần Thơ');
});

Ví dụ 5: So sánh PUT vs PATCH trong thực tế

// Dữ liệu gốc
{
  "id": 1,
  "name": "Nguyễn Văn A",
  "email": "vana@example.com",
  "age": 25,
  "city": "Hà Nội",
  "phone": "0123456789"
}

// ❌ Dùng PUT sai cách (thiếu fields)
PUT /api/users/1
{
  "email": "new@example.com"
}
// Kết quả: Mất name, age, city, phone!

// ✅ Dùng PUT đúng cách (gửi tất cả)
PUT /api/users/1
{
  "name": "Nguyễn Văn A",
  "email": "new@example.com",
  "age": 25,
  "city": "Hà Nội",
  "phone": "0123456789"
}
// Kết quả: Tất cả fields đều có

// ✅ Dùng PATCH (chỉ gửi field cần đổi)
PATCH /api/users/1
{
  "email": "new@example.com"
}
// Kết quả: Chỉ email đổi, còn lại giữ nguyên!

DELETE Phương thức DELETE

Mục đích: Xóa một tài nguyên trên server.

Đặc điểm:

  • ✗ Không safe: Thay đổi trạng thái server
  • ✓ Idempotent: Xóa nhiều lần cho cùng kết quả (tài nguyên vẫn không tồn tại)
  • ✓ Thường không có request body
  • ✓ Có thể trả về thông tin tài nguyên đã xóa
Response codes phổ biến:
  • 200 OK: Xóa thành công và trả về thông tin tài nguyên đã xóa
  • 204 No Content: Xóa thành công, không trả về body
  • 202 Accepted: Yêu cầu xóa được chấp nhận nhưng chưa thực hiện
  • 404 Not Found: Tài nguyên không tồn tại (lần đầu xóa thì 200/204, lần sau thì 404)
Khi nào sử dụng DELETE?
  • Xóa user, sản phẩm, bài viết
  • Hủy đơn hàng
  • Xóa comment, review
  • Bất kỳ thao tác xóa tài nguyên nào
⚠️ Lưu ý về tính idempotent:
  • DELETE lần 1: 200/204 (xóa thành công)
  • DELETE lần 2: 404 (không tìm thấy) - vẫn coi là idempotent vì kết quả cuối cùng giống nhau (tài nguyên không tồn tại)
  • Một số API có thể trả về 204 cho cả 2 lần để giữ tính idempotent hoàn toàn
⚠️ Best practices:
  • Cân nhắc soft delete (đánh dấu xóa) thay vì hard delete (xóa vĩnh viễn)
  • Yêu cầu xác nhận trước khi xóa
  • Kiểm tra quyền trước khi cho phép xóa
  • Log lại thao tác xóa để audit

Ví dụ 1: Xóa user (trả về 204)

DELETE /api/users/1 HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here

Response:

HTTP/1.1 204 No Content

Ví dụ 2: Xóa user (trả về 200 với data)

DELETE /api/users/1 HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "User đã được xóa thành công",
  "deleted_user": {
    "id": 1,
    "name": "Nguyễn Văn A",
    "email": "vana@example.com"
  },
  "deleted_at": "2025-01-15T13:00:00Z"
}

Ví dụ 3: Xóa tài nguyên không tồn tại

DELETE /api/users/99999 HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here

Response:

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": "User không tồn tại",
  "user_id": 99999
}

Ví dụ 4: Soft delete

DELETE /api/posts/123 HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "title": "Bài viết của tôi",
  "status": "deleted",
  "deleted_at": "2025-01-15T13:30:00Z",
  "can_restore": true
}

⚠️ Soft delete: Bài viết không bị xóa vĩnh viễn, chỉ đánh dấu là đã xóa

Ví dụ 5: Xóa với cascade

DELETE /api/users/1?cascade=true HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "User và dữ liệu liên quan đã được xóa",
  "deleted": {
    "user": 1,
    "posts": 15,
    "comments": 42,
    "likes": 128
  }
}

Ví dụ 6: Test DELETE với Playwright

import { test, expect } from '@playwright/test';

test('DELETE - Xóa user thành công', async ({ request }) => {
  // Tạo user mới để test
  const createResponse = await request.post('https://example.com/api/users', {
    data: {
      name: 'Test User',
      email: 'test@example.com'
    }
  });
  const user = await createResponse.json();

  // Xóa user vừa tạo
  const deleteResponse = await request.delete(`https://example.com/api/users/${user.id}`);
  expect(deleteResponse.status()).toBe(204);

  // Verify user đã bị xóa
  const getResponse = await request.get(`https://example.com/api/users/${user.id}`);
  expect(getResponse.status()).toBe(404);
});

test('DELETE - Tính idempotent', async ({ request }) => {
  // Tạo user
  const createResponse = await request.post('https://example.com/api/users', {
    data: {
      name: 'Test Delete',
      email: 'testdelete@example.com'
    }
  });
  const user = await createResponse.json();

  // Xóa lần 1
  const delete1 = await request.delete(`https://example.com/api/users/${user.id}`);
  expect([200, 204]).toContain(delete1.status());

  // Xóa lần 2 (idempotent)
  const delete2 = await request.delete(`https://example.com/api/users/${user.id}`);
  expect([404, 204]).toContain(delete2.status());
  // Cả 2 đều ok: 404 (không tìm thấy) hoặc 204 (xóa lại cũng ok)
});

test('DELETE - Xóa không có quyền', async ({ request }) => {
  const response = await request.delete('https://example.com/api/users/1', {
    headers: {
      'Authorization': 'Bearer invalid-token'
    }
  });

  expect(response.status()).toBe(401); // Unauthorized
});

Ví dụ 7: Bulk delete (xóa nhiều)

DELETE /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer your-token-here

{
  "user_ids": [1, 2, 3, 4, 5]
}

Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "Đã xóa 5 users",
  "deleted_count": 5,
  "deleted_ids": [1, 2, 3, 4, 5]
}

HEAD Phương thức HEAD

Mục đích: Giống GET nhưng chỉ trả về headers, không trả về body.

Đặc điểm:

  • ✓ Safe: Không làm thay đổi trạng thái server
  • ✓ Idempotent: Gọi nhiều lần cho cùng kết quả
  • ✓ Giống hệt GET nhưng không có response body
  • ✓ Nhanh hơn GET vì không tải body
  • ✓ Headers trả về giống hệt như GET
Khi nào sử dụng HEAD?
  • Kiểm tra xem resource có tồn tại không
  • Lấy metadata (size, last-modified, content-type) mà không cần tải toàn bộ content
  • Kiểm tra xem file có thay đổi không (qua Last-Modified hoặc ETag)
  • Kiểm tra link có còn hoạt động không
  • Tiết kiệm bandwidth khi chỉ cần thông tin headers
⚠️ Lưu ý:
  • Server phải xử lý HEAD giống hệt GET, chỉ khác là không gửi body
  • Không phải tất cả APIs đều hỗ trợ HEAD
  • Headers trong HEAD response phải giống hệt GET response

Ví dụ 1: Kiểm tra resource có tồn tại

HEAD /api/users/1 HTTP/1.1
Host: example.com

Response (nếu tồn tại):

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 245
Last-Modified: Mon, 15 Jan 2025 10:00:00 GMT
ETag: "abc123"

(không có body)

Response (nếu không tồn tại):

HTTP/1.1 404 Not Found

(không có body)

Ví dụ 2: Kiểm tra file size trước khi download

HEAD /api/files/large-video.mp4 HTTP/1.1
Host: example.com

Response:

HTTP/1.1 200 OK
Content-Type: video/mp4
Content-Length: 524288000
Last-Modified: Sun, 14 Jan 2025 15:30:00 GMT
Accept-Ranges: bytes

(không có body - đã biết file 500MB mà không cần tải)

Ví dụ 3: So sánh GET vs HEAD

GET Request:

GET /api/users/1 HTTP/1.1
Host: example.com

GET Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 125

{
  "id": 1,
  "name": "Nguyễn Văn A",
  "email": "vana@example.com"
}

HEAD Request:

HEAD /api/users/1 HTTP/1.1
Host: example.com

HEAD Response:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 125

(không có body, nhưng headers giống hệt GET)

Ví dụ 4: Kiểm tra file có thay đổi không

// Lần 1: Lấy ETag
HEAD /api/documents/report.pdf HTTP/1.1
Host: example.com

Response:
HTTP/1.1 200 OK
ETag: "v1-abc123"
Last-Modified: Mon, 15 Jan 2025 09:00:00 GMT

// Lần 2: Kiểm tra sau 1 giờ
HEAD /api/documents/report.pdf HTTP/1.1
Host: example.com
If-None-Match: "v1-abc123"

Response:
HTTP/1.1 304 Not Modified
ETag: "v1-abc123"

// => File chưa thay đổi, không cần tải lại!

Ví dụ 5: Test HEAD với Playwright

import { test, expect } from '@playwright/test';

test('HEAD - Kiểm tra user tồn tại', async ({ request }) => {
  const response = await request.head('https://example.com/api/users/1');

  expect(response.status()).toBe(200);
  expect(response.headers()['content-type']).toContain('application/json');

  // Verify không có body
  const body = await response.text();
  expect(body).toBe('');
});

test('HEAD - Kiểm tra file size', async ({ request }) => {
  const response = await request.head('https://example.com/api/files/video.mp4');

  expect(response.status()).toBe(200);

  const contentLength = response.headers()['content-length'];
  const fileSizeMB = parseInt(contentLength) / (1024 * 1024);

  console.log(`File size: ${fileSizeMB.toFixed(2)} MB`);

  // Quyết định có download không dựa vào size
  if (fileSizeMB < 100) {
    console.log('File nhỏ, có thể download');
  } else {
    console.log('File lớn, cân nhắc trước khi download');
  }
});

test('HEAD - So sánh với GET', async ({ request }) => {
  // HEAD request
  const headResponse = await request.head('https://example.com/api/users/1');
  const headHeaders = headResponse.headers();

  // GET request
  const getResponse = await request.get('https://example.com/api/users/1');
  const getHeaders = getResponse.headers();

  // Headers phải giống nhau
  expect(headHeaders['content-type']).toBe(getHeaders['content-type']);
  expect(headHeaders['content-length']).toBe(getHeaders['content-length']);

  // HEAD không có body
  const headBody = await headResponse.text();
  expect(headBody).toBe('');

  // GET có body
  const getBody = await getResponse.text();
  expect(getBody).not.toBe('');
});

Ví dụ 6: Use case thực tế - Link checker

import { test } from '@playwright/test';

test('Kiểm tra tất cả links trên trang', async ({ request, page }) => {
  await page.goto('https://example.com');

  // Lấy tất cả links
  const links = await page.$$eval('a', anchors =>
    anchors.map(a => a.href).filter(href => href.startsWith('http'))
  );

  console.log(`Tìm thấy ${links.length} links`);

  // Kiểm tra từng link bằng HEAD (nhanh hơn GET)
  for (const link of links) {
    try {
      const response = await request.head(link, { timeout: 5000 });

      if (response.status() >= 400) {
        console.log(`❌ Broken link: ${link} (${response.status()})`);
      } else {
        console.log(`✓ OK: ${link}`);
      }
    } catch (error) {
      console.log(`❌ Error checking ${link}: ${error.message}`);
    }
  }
});

OPTIONS Phương thức OPTIONS

Mục đích: Lấy thông tin về các phương thức HTTP và các options được hỗ trợ cho một resource.

Đặc điểm:

  • ✓ Safe: Không làm thay đổi trạng thái server
  • ✓ Idempotent: Gọi nhiều lần cho cùng kết quả
  • ✓ Sử dụng chủ yếu cho CORS preflight requests
  • ✓ Trả về các methods được phép trong header Allow
CORS Preflight Request:

Khi browser gửi cross-origin request với:

  • Methods khác GET/HEAD/POST
  • Custom headers
  • Content-Type khác application/x-www-form-urlencoded, multipart/form-data, text/plain

Browser sẽ tự động gửi OPTIONS request trước (preflight) để kiểm tra xem server có cho phép không.

Khi nào sử dụng OPTIONS?
  • Kiểm tra API hỗ trợ những methods nào
  • CORS preflight (browser tự động gửi)
  • Kiểm tra server capabilities
  • API discovery
⚠️ Response Headers quan trọng:
  • Allow: Các methods được hỗ trợ
  • Access-Control-Allow-Methods: Methods cho phép trong CORS
  • Access-Control-Allow-Headers: Headers cho phép trong CORS
  • Access-Control-Allow-Origin: Origins được phép
  • Access-Control-Max-Age: Thời gian cache preflight

Ví dụ 1: Kiểm tra methods được hỗ trợ

OPTIONS /api/users/1 HTTP/1.1
Host: example.com

Response:

HTTP/1.1 200 OK
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 0

=> Endpoint này hỗ trợ GET, PUT, PATCH, DELETE, HEAD, OPTIONS

Ví dụ 2: CORS Preflight Request

Browser tự động gửi OPTIONS trước khi gửi DELETE:

OPTIONS /api/users/1 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

Server Response:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

=> Server cho phép, browser sẽ gửi DELETE request tiếp theo

Ví dụ 3: OPTIONS cho toàn bộ API

OPTIONS /api HTTP/1.1
Host: example.com

Response:

HTTP/1.1 200 OK
Allow: GET, POST, OPTIONS
Content-Type: application/json

{
  "version": "1.0",
  "endpoints": {
    "/api/users": ["GET", "POST"],
    "/api/users/:id": ["GET", "PUT", "PATCH", "DELETE"],
    "/api/products": ["GET", "POST"],
    "/api/orders": ["GET", "POST"]
  }
}

Ví dụ 4: CORS bị reject

Preflight request:

OPTIONS /api/admin/users HTTP/1.1
Host: api.example.com
Origin: https://untrusted-site.com
Access-Control-Request-Method: DELETE

Server Response (reject):

HTTP/1.1 403 Forbidden

(hoặc không có Access-Control-Allow-Origin header)

=> Browser sẽ chặn và không gửi DELETE request

Ví dụ 5: Test OPTIONS với Playwright

import { test, expect } from '@playwright/test';

test('OPTIONS - Kiểm tra methods được hỗ trợ', async ({ request }) => {
  const response = await request.fetch('https://example.com/api/users/1', {
    method: 'OPTIONS'
  });

  expect(response.status()).toBe(200);

  const allowHeader = response.headers()['allow'];
  expect(allowHeader).toBeTruthy();

  const methods = allowHeader.split(',').map(m => m.trim());
  console.log('Supported methods:', methods);

  expect(methods).toContain('GET');
  expect(methods).toContain('DELETE');
});

test('OPTIONS - CORS preflight', async ({ request }) => {
  const response = await request.fetch('https://api.example.com/users', {
    method: 'OPTIONS',
    headers: {
      'Origin': 'https://myapp.com',
      'Access-Control-Request-Method': 'DELETE',
      'Access-Control-Request-Headers': 'Authorization'
    }
  });

  const headers = response.headers();

  // Kiểm tra CORS headers
  expect(headers['access-control-allow-origin']).toBeTruthy();
  expect(headers['access-control-allow-methods']).toContain('DELETE');
  expect(headers['access-control-allow-headers']).toContain('Authorization');
});

test('OPTIONS - API discovery', async ({ request }) => {
  const response = await request.fetch('https://example.com/api', {
    method: 'OPTIONS'
  });

  expect(response.status()).toBe(200);

  const data = await response.json();
  expect(data).toHaveProperty('endpoints');

  console.log('Available endpoints:', data.endpoints);
});

Ví dụ 6: Flow CORS hoàn chỉnh

// Bước 1: User click button "Delete" trên frontend (https://myapp.com)
// Browser tự động gửi OPTIONS preflight

OPTIONS /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

// Bước 2: Server response cho phép

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 3600

// Bước 3: Browser thấy được phép, gửi DELETE request thật

DELETE /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Authorization: Bearer token123

// Bước 4: Server xử lý và response

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com

// ✓ Hoàn tất!

Ví dụ 7: Test CORS với Playwright

import { test, expect } from '@playwright/test';

test('Test CORS flow hoàn chỉnh', async ({ page, request }) => {
  // Mô phỏng frontend gửi request từ domain khác

  // 1. Preflight
  const preflightResponse = await request.fetch('https://api.example.com/users/1', {
    method: 'OPTIONS',
    headers: {
      'Origin': 'https://myapp.com',
      'Access-Control-Request-Method': 'DELETE'
    }
  });

  console.log('Preflight response:', preflightResponse.status());
  expect(preflightResponse.status()).toBe(204);

  // 2. Kiểm tra CORS headers
  const corsHeaders = preflightResponse.headers();
  expect(corsHeaders['access-control-allow-origin']).toBe('https://myapp.com');
  expect(corsHeaders['access-control-allow-methods']).toContain('DELETE');

  // 3. Nếu preflight pass, gửi request thật
  const deleteResponse = await request.delete('https://api.example.com/users/1', {
    headers: {
      'Origin': 'https://myapp.com'
    }
  });

  expect(deleteResponse.status()).toBe(204);
  expect(deleteResponse.headers()['access-control-allow-origin']).toBe('https://myapp.com');
});