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.
| 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 | ✓ | ✓ | - |
| Kiểm tra các methods được hỗ trợ | ✓ | ✓ | - |
Mục đích: Lấy dữ liệu từ server mà không làm thay đổi dữ liệu.
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"
}
]
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"
}
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"
}
]
}
// 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');
});
Mục đích: Tạo mới tài nguyên hoặc gửi dữ liệu để xử lý trên server.
Location với URL của tài nguyên mớiPOST /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"
}
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"
}
}
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
}
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');
});
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.
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"
}
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"
}
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"
}
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);
});
Mục đích: Cập nhật một phần (partial update) tài nguyên.
| Đặ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 | Có | Tùy implementation |
| Fields không gửi | Bị xóa/reset | Giữ nguyên |
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!
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"
}
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!
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ơ');
});
// 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!
Mục đích: Xóa một tài nguyên trên server.
DELETE /api/users/1 HTTP/1.1
Host: example.com
Authorization: Bearer your-token-here
Response:
HTTP/1.1 204 No Content
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"
}
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
}
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
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
}
}
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
});
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]
}
Mục đích: Giống GET nhưng chỉ trả về headers, không trả về body.
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)
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)
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)
// 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!
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('');
});
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}`);
}
}
});
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.
AllowKhi browser gửi cross-origin request với:
Browser sẽ tự động gửi OPTIONS request trước (preflight) để kiểm tra xem server có cho phép không.
Allow: Các methods được hỗ trợAccess-Control-Allow-Methods: Methods cho phép trong CORSAccess-Control-Allow-Headers: Headers cho phép trong CORSAccess-Control-Allow-Origin: Origins được phépAccess-Control-Max-Age: Thời gian cache preflightOPTIONS /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
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
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"]
}
}
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
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);
});
// 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!
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');
});