Response Body là phần dữ liệu mà server trả về cho client sau khi xử lý một HTTP request. Đây là nơi chứa thông tin chính mà client cần.
HTTP/1.1 200 OK ← Status Line
Content-Type: application/json ← Headers
Content-Length: 85
Date: Mon, 15 Jan 2025 10:30:00 GMT
{ ← Response Body (bắt đầu)
"id": 1,
"name": "John Doe",
"email": "john@example.com"
} ← Response Body (kết thúc)
| Thành phần | Mô tả | Ví dụ |
|---|---|---|
| Status Line | HTTP version, status code, status text | HTTP/1.1 200 OK |
| Headers | Metadata về response | Content-Type: application/json |
| Body | Dữ liệu chính được trả về | {"id": 1, "name": "John"} |
import { test, expect } from '@playwright/test';
test('Đọc response body', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
// Kiểm tra status
expect(response.status()).toBe(200);
// Đọc body dạng JSON
const body = await response.json();
// Verify body data
expect(body).toHaveProperty('id');
expect(body).toHaveProperty('name');
expect(body).toHaveProperty('email');
console.log('Response body:', body);
});
test('Response không có body', async ({ request }) => {
const response = await request.delete('https://api.example.com/users/1');
// 204 No Content - không có body
expect(response.status()).toBe(204);
// Body rỗng
const text = await response.text();
expect(text).toBe('');
});
JSON là format phổ biến nhất cho API responses. Dễ đọc, dễ parse, và được hỗ trợ bởi hầu hết các ngôn ngữ.
Content-Type: application/json
// Object
{
"key": "value",
"number": 123,
"boolean": true,
"null_value": null
}
// Array
[
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"}
]
// Nested structure
{
"user": {
"id": 1,
"name": "John",
"address": {
"city": "Hanoi",
"country": "Vietnam"
}
},
"orders": [
{"id": 101, "total": 50000},
{"id": 102, "total": 75000}
]
}
| Type | Ví dụ | Ghi chú |
|---|---|---|
| String | "Hello World" | Phải dùng double quotes |
| Number | 123, 45.67, -89 | Integer hoặc float |
| Boolean | true, false | Lowercase |
| Null | null | Lowercase |
| Array | [1, 2, 3] | Ordered list |
| Object | {"key": "value"} | Key-value pairs |
import { test, expect } from '@playwright/test';
test('Parse JSON response', async ({ request }) => {
const response = await request.get('https://api.example.com/users');
// Verify Content-Type
expect(response.headers()['content-type']).toContain('application/json');
// Parse JSON
const users = await response.json();
// Verify structure
expect(users).toBeInstanceOf(Array);
expect(users.length).toBeGreaterThan(0);
// Verify first user
const firstUser = users[0];
expect(firstUser).toHaveProperty('id');
expect(typeof firstUser.id).toBe('number');
expect(firstUser).toHaveProperty('name');
expect(typeof firstUser.name).toBe('string');
});
test('Handle nested JSON', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const user = await response.json();
// Access nested properties
expect(user.address.city).toBe('Hanoi');
expect(user.orders[0].total).toBeGreaterThan(0);
});
XML là format cũ hơn, vẫn được dùng trong enterprise systems, SOAP APIs, và một số legacy systems.
Content-Type: application/xml hoặc text/xml
<?xml version="1.0" encoding="UTF-8"?>
<user>
<id>1</id>
<name>John Doe</name>
<email>john@example.com</email>
<address>
<city>Hanoi</city>
<country>Vietnam</country>
</address>
<orders>
<order id="101">
<total>50000</total>
</order>
<order id="102">
<total>75000</total>
</order>
</orders>
</user>
import { test, expect } from '@playwright/test';
test('Parse XML response', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1.xml');
// Verify Content-Type
const contentType = response.headers()['content-type'];
expect(contentType).toMatch(/xml/);
// Get as text
const xmlText = await response.text();
// Simple verification using regex (hoặc dùng XML parser library)
expect(xmlText).toContain('<user>');
expect(xmlText).toContain('<name>John Doe</name>');
});
// Với xml2js library (cần install)
import { parseStringPromise } from 'xml2js';
test('Parse XML with library', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1.xml');
const xmlText = await response.text();
// Parse XML to JSON
const result = await parseStringPromise(xmlText);
expect(result.user.name[0]).toBe('John Doe');
expect(result.user.email[0]).toBe('john@example.com');
});
| Aspect | JSON | XML |
|---|---|---|
| Readability | Dễ đọc hơn | Verbose hơn |
| Size | Nhỏ hơn | Lớn hơn |
| Parsing | Native trong JS | Cần library |
| Schema | JSON Schema | XSD, DTD |
| Use case | REST APIs, Web | SOAP, Enterprise |
Content-Type: text/html
<!DOCTYPE html>
<html>
<head><title>User Profile</title></head>
<body>
<h1>John Doe</h1>
<p>Email: john@example.com</p>
</body>
</html>
Content-Type: text/plain
OK
User created successfully
Error: Invalid input
Content-Types:
application/pdf - PDF filesimage/png, image/jpeg - Imagesapplication/octet-stream - Generic binaryapplication/zip - ZIP filesimport { test, expect } from '@playwright/test';
// HTML response
test('Parse HTML response', async ({ request }) => {
const response = await request.get('https://example.com/page');
const html = await response.text();
expect(html).toContain('<title>');
expect(html).toContain('</html>');
});
// Plain text response
test('Parse plain text response', async ({ request }) => {
const response = await request.get('https://api.example.com/health');
const text = await response.text();
expect(text).toBe('OK');
});
// Binary response (image)
test('Download image', async ({ request }) => {
const response = await request.get('https://api.example.com/avatar/1');
expect(response.headers()['content-type']).toContain('image/');
// Get as buffer
const buffer = await response.body();
expect(buffer.length).toBeGreaterThan(0);
});
// PDF response
test('Download PDF', async ({ request }) => {
const response = await request.get('https://api.example.com/reports/123.pdf');
expect(response.headers()['content-type']).toContain('application/pdf');
const buffer = await response.body();
// PDF files start with %PDF
expect(buffer.toString().startsWith('%PDF')).toBeTruthy();
});
// Simple: Trả về object trực tiếp
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2025-01-15T10:30:00Z"
}
// Wrapped: Object nằm trong "data" field
{
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
"meta": {
"request_id": "abc123",
"timestamp": "2025-01-15T10:30:00Z"
}
}
// Include related resources
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"department": {
"id": 10,
"name": "Engineering"
},
"manager": {
"id": 5,
"name": "Jane Smith"
},
"roles": ["admin", "developer"]
}
import { test, expect } from '@playwright/test';
test('GET single user', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
expect(response.status()).toBe(200);
const user = await response.json();
// Verify required fields
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
// Verify data types
expect(typeof user.id).toBe('number');
expect(typeof user.name).toBe('string');
expect(user.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
// Verify optional related data
if (user.department) {
expect(user.department).toHaveProperty('id');
expect(user.department).toHaveProperty('name');
}
});
// Trả về array trực tiếp
[
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"},
{"id": 3, "name": "Bob"}
]
// Array trong "data" field + pagination info
{
"data": [
{"id": 1, "name": "John"},
{"id": 2, "name": "Jane"},
{"id": 3, "name": "Bob"}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 157,
"total_pages": 8
}
}
{
"data": [...],
"links": {
"self": "/api/users?page=2",
"first": "/api/users?page=1",
"prev": "/api/users?page=1",
"next": "/api/users?page=3",
"last": "/api/users?page=8"
},
"meta": {
"current_page": 2,
"per_page": 20,
"total": 157,
"total_pages": 8
}
}
{
"data": [...],
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true
}
import { test, expect } from '@playwright/test';
test('GET users list with pagination', async ({ request }) => {
const response = await request.get('https://api.example.com/users', {
params: { page: 1, limit: 20 }
});
expect(response.status()).toBe(200);
const result = await response.json();
// Verify data array
expect(result.data).toBeInstanceOf(Array);
expect(result.data.length).toBeLessThanOrEqual(20);
// Verify each item in array
result.data.forEach(user => {
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
});
// Verify pagination
expect(result.pagination).toHaveProperty('page', 1);
expect(result.pagination).toHaveProperty('total');
expect(result.pagination).toHaveProperty('total_pages');
});
test('Verify empty collection', async ({ request }) => {
const response = await request.get('https://api.example.com/users', {
params: { status: 'nonexistent' }
});
expect(response.status()).toBe(200);
const result = await response.json();
// Empty array, not 404
expect(result.data).toEqual([]);
expect(result.pagination.total).toBe(0);
});
Tất cả responses đều có cùng structure:
// Success response
{
"success": true,
"data": {
"id": 1,
"name": "John Doe"
},
"message": "User retrieved successfully",
"timestamp": "2025-01-15T10:30:00Z"
}
// Error response
{
"success": false,
"data": null,
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 999 not found"
},
"timestamp": "2025-01-15T10:30:00Z"
}
// Success
{
"status": "success",
"data": {
"user": {"id": 1, "name": "John"}
}
}
// Fail (validation error)
{
"status": "fail",
"data": {
"email": "Invalid email format",
"password": "Password too short"
}
}
// Error (server error)
{
"status": "error",
"message": "Internal server error",
"code": 500
}
import { test, expect } from '@playwright/test';
test('Verify envelope structure - success', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const body = await response.json();
// Verify envelope structure
expect(body).toHaveProperty('success', true);
expect(body).toHaveProperty('data');
expect(body).toHaveProperty('timestamp');
// Verify actual data
expect(body.data).toHaveProperty('id', 1);
});
test('Verify envelope structure - error', async ({ request }) => {
const response = await request.get('https://api.example.com/users/99999');
expect(response.status()).toBe(404);
const body = await response.json();
// Verify error envelope
expect(body).toHaveProperty('success', false);
expect(body).toHaveProperty('error');
expect(body.error).toHaveProperty('code');
expect(body.error).toHaveProperty('message');
expect(body.data).toBeNull();
});
| Status | Tên | Response Body | Use case |
|---|---|---|---|
| 200 | OK | Có data | GET, PUT, PATCH success |
| 201 | Created | Created resource | POST success |
| 202 | Accepted | Status info | Async processing started |
| 204 | No Content | Empty | DELETE success |
// 200 OK - GET user
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
// 201 Created - POST new user
{
"id": 10,
"name": "New User",
"email": "newuser@example.com",
"created_at": "2025-01-15T10:30:00Z"
}
// 202 Accepted - Async job
{
"job_id": "abc123",
"status": "processing",
"check_url": "/api/jobs/abc123"
}
// 204 No Content - DELETE
(empty body)
| Status | Tên | Response Body | Use case |
|---|---|---|---|
| 400 | Bad Request | Validation errors | Invalid input |
| 401 | Unauthorized | Auth error | Missing/invalid token |
| 403 | Forbidden | Permission error | Không có quyền |
| 404 | Not Found | Error message | Resource không tồn tại |
| 409 | Conflict | Conflict details | Duplicate, conflict |
| 422 | Unprocessable Entity | Validation errors | Semantic errors |
| 429 | Too Many Requests | Rate limit info | Rate limited |
// 400 Bad Request
{
"error": "Bad Request",
"message": "Invalid JSON format",
"details": "Unexpected token at position 15"
}
// 401 Unauthorized
{
"error": "Unauthorized",
"message": "Invalid or expired access token",
"code": "TOKEN_EXPIRED"
}
// 403 Forbidden
{
"error": "Forbidden",
"message": "You don't have permission to access this resource",
"required_role": "admin"
}
// 404 Not Found
{
"error": "Not Found",
"message": "User with ID 999 not found"
}
// 422 Validation Error
{
"error": "Validation Error",
"message": "Input validation failed",
"errors": [
{"field": "email", "message": "Invalid email format"},
{"field": "password", "message": "Must be at least 8 characters"}
]
}
// 429 Rate Limited
{
"error": "Too Many Requests",
"message": "Rate limit exceeded",
"retry_after": 60
}
| Status | Tên | Response Body |
|---|---|---|
| 500 | Internal Server Error | Generic error (không expose details) |
| 502 | Bad Gateway | Upstream error |
| 503 | Service Unavailable | Maintenance info |
| 504 | Gateway Timeout | Timeout info |
// 500 Internal Server Error
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"request_id": "abc123" // Để support team trace
}
// 503 Service Unavailable
{
"error": "Service Unavailable",
"message": "Service is under maintenance",
"retry_after": 3600
}
import { test, expect } from '@playwright/test';
test('Test various status codes', async ({ request }) => {
// 200 OK
const res200 = await request.get('https://api.example.com/users/1');
expect(res200.status()).toBe(200);
expect(res200.ok()).toBeTruthy();
// 201 Created
const res201 = await request.post('https://api.example.com/users', {
data: { name: 'New User', email: 'new@example.com' }
});
expect(res201.status()).toBe(201);
const created = await res201.json();
expect(created).toHaveProperty('id');
// 204 No Content
const res204 = await request.delete('https://api.example.com/users/1');
expect(res204.status()).toBe(204);
expect(await res204.text()).toBe('');
// 404 Not Found
const res404 = await request.get('https://api.example.com/users/99999');
expect(res404.status()).toBe(404);
expect(res404.ok()).toBeFalsy();
const error404 = await res404.json();
expect(error404).toHaveProperty('error');
// 422 Validation Error
const res422 = await request.post('https://api.example.com/users', {
data: { name: '', email: 'invalid-email' }
});
expect(res422.status()).toBe(422);
const error422 = await res422.json();
expect(error422.errors).toBeInstanceOf(Array);
});
{
"error": "Not Found",
"message": "User with ID 999 not found"
}
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User with ID 999 not found",
"status": 404,
"timestamp": "2025-01-15T10:30:00Z",
"path": "/api/users/999",
"request_id": "abc123"
}
}
{
"type": "https://api.example.com/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "User with ID 999 does not exist",
"instance": "/api/users/999"
}
| Field | Mô tả | Required? |
|---|---|---|
error / code |
Error type/code | Yes |
message |
Human-readable message | Yes |
status |
HTTP status code | Optional |
details |
Additional info | Optional |
request_id |
For debugging/support | Recommended |
{
"error": "Validation Error",
"message": "Input validation failed",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
},
{
"field": "password",
"message": "Must be at least 8 characters",
"code": "TOO_SHORT",
"min_length": 8
},
{
"field": "age",
"message": "Must be a positive number",
"code": "INVALID_VALUE"
}
]
}
{
"error": "Validation Error",
"errors": {
"email": ["Invalid email format", "Email already exists"],
"password": ["Must be at least 8 characters"],
"age": ["Must be a positive number"]
}
}
{
"error": "Validation Error",
"errors": {
"user.name": ["Name is required"],
"user.address.city": ["City is required"],
"items[0].quantity": ["Must be at least 1"],
"items[1].price": ["Invalid price format"]
}
}
import { test, expect } from '@playwright/test';
test('Test validation errors', async ({ request }) => {
const response = await request.post('https://api.example.com/users', {
data: {
name: '', // Empty - invalid
email: 'not-email', // Invalid format
age: -5 // Negative - invalid
}
});
expect(response.status()).toBe(422);
const body = await response.json();
// Verify error structure
expect(body).toHaveProperty('error', 'Validation Error');
expect(body).toHaveProperty('errors');
expect(body.errors).toBeInstanceOf(Array);
// Verify specific errors
const fieldErrors = body.errors.map(e => e.field);
expect(fieldErrors).toContain('name');
expect(fieldErrors).toContain('email');
expect(fieldErrors).toContain('age');
// Find specific error
const emailError = body.errors.find(e => e.field === 'email');
expect(emailError.message).toContain('email');
});
// Missing token
{
"error": "Unauthorized",
"message": "Authentication required",
"code": "AUTH_REQUIRED"
}
// Invalid token
{
"error": "Unauthorized",
"message": "Invalid access token",
"code": "INVALID_TOKEN"
}
// Expired token
{
"error": "Unauthorized",
"message": "Access token has expired",
"code": "TOKEN_EXPIRED",
"expired_at": "2025-01-15T10:30:00Z"
}
// Insufficient permissions
{
"error": "Forbidden",
"message": "You don't have permission to perform this action",
"code": "INSUFFICIENT_PERMISSIONS",
"required_permission": "users:delete"
}
// Role-based restriction
{
"error": "Forbidden",
"message": "Admin role required",
"code": "ROLE_REQUIRED",
"required_role": "admin",
"current_role": "user"
}
// Resource ownership
{
"error": "Forbidden",
"message": "You can only modify your own resources",
"code": "OWNERSHIP_REQUIRED"
}
import { test, expect } from '@playwright/test';
test('Test 401 - No token', async ({ request }) => {
const response = await request.get('https://api.example.com/protected');
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.code).toBe('AUTH_REQUIRED');
});
test('Test 401 - Invalid token', async ({ request }) => {
const response = await request.get('https://api.example.com/protected', {
headers: {
'Authorization': 'Bearer invalid-token'
}
});
expect(response.status()).toBe(401);
const body = await response.json();
expect(body.code).toBe('INVALID_TOKEN');
});
test('Test 403 - Insufficient permissions', async ({ request }) => {
// Login as normal user
const loginRes = await request.post('https://api.example.com/login', {
data: { email: 'user@example.com', password: 'password' }
});
const { token } = await loginRes.json();
// Try to access admin-only resource
const response = await request.delete('https://api.example.com/admin/users/1', {
headers: {
'Authorization': `Bearer ${token}`
}
});
expect(response.status()).toBe(403);
const body = await response.json();
expect(body.code).toBe('INSUFFICIENT_PERMISSIONS');
});
| Method | Return Type | Use case |
|---|---|---|
response.json() |
Object/Array | JSON responses |
response.text() |
String | Text, HTML, XML |
response.body() |
Buffer | Binary data (images, PDFs) |
import { test, expect } from '@playwright/test';
test('Parse JSON response', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
// Parse as JSON
const user = await response.json();
expect(user.id).toBe(1);
expect(user.name).toBeDefined();
});
test('Parse text response', async ({ request }) => {
const response = await request.get('https://api.example.com/health');
// Parse as text
const text = await response.text();
expect(text).toBe('OK');
});
test('Handle binary response', async ({ request }) => {
const response = await request.get('https://api.example.com/files/image.png');
// Get as buffer
const buffer = await response.body();
expect(buffer.length).toBeGreaterThan(0);
// PNG magic bytes
expect(buffer[0]).toBe(0x89);
expect(buffer[1]).toBe(0x50); // 'P'
expect(buffer[2]).toBe(0x4E); // 'N'
expect(buffer[3]).toBe(0x47); // 'G'
});
import { test, expect } from '@playwright/test';
test('Verify complete response structure', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const user = await response.json();
// Check required fields exist
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
expect(user).toHaveProperty('created_at');
// Check data types
expect(typeof user.id).toBe('number');
expect(typeof user.name).toBe('string');
expect(typeof user.email).toBe('string');
// Check format
expect(user.email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
expect(user.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
// Check value ranges
expect(user.id).toBeGreaterThan(0);
expect(user.name.length).toBeGreaterThan(0);
});
test('Verify array response', async ({ request }) => {
const response = await request.get('https://api.example.com/users');
const result = await response.json();
// Verify pagination structure
expect(result).toHaveProperty('data');
expect(result).toHaveProperty('pagination');
// Verify data array
expect(Array.isArray(result.data)).toBeTruthy();
// Verify each item
for (const user of result.data) {
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('name');
expect(typeof user.id).toBe('number');
}
// Verify pagination
expect(result.pagination).toMatchObject({
page: expect.any(Number),
per_page: expect.any(Number),
total: expect.any(Number)
});
});
test('Handle optional fields', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const user = await response.json();
// Required fields
expect(user.id).toBeDefined();
expect(user.name).toBeDefined();
// Optional fields - check if exists before asserting
if (user.phone) {
expect(user.phone).toMatch(/^\+?[\d\s-]+$/);
}
if (user.address) {
expect(user.address).toHaveProperty('city');
expect(user.address).toHaveProperty('country');
}
// Or use optional chaining
expect(user.profile?.bio ?? '').toBeDefined();
});
test('Compare response với expected data', async ({ request }) => {
// Create user
const createRes = await request.post('https://api.example.com/users', {
data: {
name: 'John Doe',
email: 'john@example.com'
}
});
const created = await createRes.json();
// Verify response matches input
expect(created).toMatchObject({
name: 'John Doe',
email: 'john@example.com'
});
// Verify server-generated fields
expect(created.id).toBeDefined();
expect(created.created_at).toBeDefined();
});
test('Snapshot testing', async ({ request }) => {
const response = await request.get('https://api.example.com/config');
const config = await response.json();
// Compare với snapshot (cần setup)
expect(config).toMatchSnapshot();
});
test('Extract và reuse data', async ({ request }) => {
// Step 1: Create user
const createRes = await request.post('https://api.example.com/users', {
data: { name: 'Test User', email: 'test@example.com' }
});
const { id: userId } = await createRes.json();
// Step 2: Create order for user
const orderRes = await request.post('https://api.example.com/orders', {
data: {
user_id: userId, // Reuse userId
items: [{ product_id: 1, quantity: 2 }]
}
});
const { id: orderId } = await orderRes.json();
// Step 3: Get order details
const getRes = await request.get(`https://api.example.com/orders/${orderId}`);
const order = await getRes.json();
expect(order.user_id).toBe(userId);
expect(order.items.length).toBe(1);
});
// ✓ Good: Consistent structure cho tất cả endpoints
// Success
{
"data": {...},
"meta": {...}
}
// Error
{
"error": {...}
}
// ✗ Bad: Mỗi endpoint khác nhau
/users → [...]
/orders → {"orders": [...]}
/products → {"data": {"items": [...]}}
// ✓ Good: Clear, actionable message
{
"error": "Validation Error",
"message": "Email format is invalid. Expected format: user@domain.com",
"field": "email",
"provided_value": "not-an-email"
}
// ✗ Bad: Generic, unhelpful
{
"error": "Bad request"
}
// Mỗi response nên có request_id để debug
{
"data": {...},
"meta": {
"request_id": "req_abc123xyz"
}
}
// Đặc biệt quan trọng cho errors
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"request_id": "req_abc123xyz" // Support team có thể trace
}
// ✓ Good: Đúng status code
200 OK - GET success
201 Created - POST success (resource created)
204 No Content - DELETE success
400 Bad Request - Invalid input format
401 Unauthorized - Authentication required
403 Forbidden - Permission denied
404 Not Found - Resource not found
422 Unprocessable Entity - Validation failed
500 Internal Server Error - Server error
// ✗ Bad: Sai status code
200 OK với body {"error": "Not found"} // Nên là 404
500 cho validation errors // Nên là 400/422
// ✓ Good: ISO 8601 với timezone
{
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T14:45:30+07:00"
}
// ✗ Bad: Ambiguous formats
{
"created_at": "15/01/2025", // DD/MM hay MM/DD?
"updated_at": 1705312200 // Unix timestamp không rõ ràng
}
// Option 1: Include null values
{
"name": "John",
"phone": null, // Explicitly null
"address": null
}
// Option 2: Omit null values
{
"name": "John"
// phone và address không có trong response
}
// Chọn 1 approach và giữ consistent!
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 157,
"total_pages": 8,
"has_next": true,
"has_prev": true
},
"links": {
"self": "/api/users?page=2",
"first": "/api/users?page=1",
"prev": "/api/users?page=1",
"next": "/api/users?page=3",
"last": "/api/users?page=8"
}
}
// ✗ Bad: Expose internal errors
{
"error": "Database error",
"message": "SELECT * FROM users WHERE id = '1; DROP TABLE users;--'"
}
// ✓ Good: Generic message, log internally
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"request_id": "abc123" // Dùng để trace trong logs
}
// ✗ Bad: Return password/tokens
{
"id": 1,
"email": "user@example.com",
"password_hash": "$2b$10$...", // NEVER!
"api_key": "sk_live_..." // NEVER!
}
// ✓ Good: Only return safe fields
{
"id": 1,
"email": "user@example.com",
"name": "John Doe"
}
Problem: Response không phải valid JSON
// Server trả về HTML thay vì JSON (ví dụ: error page)
<html><body>Internal Server Error</body></html>
Solution:
test('Handle non-JSON response', async ({ request }) => {
const response = await request.get('https://api.example.com/endpoint');
// Check content-type trước
const contentType = response.headers()['content-type'];
if (contentType?.includes('application/json')) {
const json = await response.json();
// Process JSON
} else {
const text = await response.text();
console.log('Non-JSON response:', text);
// Handle error page, etc.
}
});
Problem: Gọi .json() trên empty body
// 204 No Content không có body
const response = await request.delete('/users/1');
const body = await response.json(); // Error!
Solution:
test('Handle empty body', async ({ request }) => {
const response = await request.delete('https://api.example.com/users/1');
if (response.status() === 204) {
// No body expected
expect(await response.text()).toBe('');
} else {
const body = await response.json();
// Process body
}
});
Problem: Response quá lớn, timeout hoặc memory issues
Solution:
test('Handle large response', async ({ request }) => {
// Use pagination
const response = await request.get('https://api.example.com/users', {
params: {
page: 1,
limit: 100 // Limit items per request
}
});
const result = await response.json();
// Stream for very large files
// (Playwright không hỗ trợ streaming, cần dùng alternatives)
});
Problem: Unicode/UTF-8 characters bị corrupt
// Tiếng Việt bị lỗi
{"name": "Nguy\u00e1\u00bb\u0085n V\u00c4\u0083n A"}
Solution:
// Server cần set đúng charset
Content-Type: application/json; charset=utf-8
// Verify trong test
test('Handle Vietnamese characters', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const user = await response.json();
// Should be properly decoded
expect(user.name).toBe('Nguyễn Văn A');
});
Problem: Cùng field có different types
// Lúc là string, lúc là number
{"id": 1} // Number
{"id": "1"} // String
// Lúc là array, lúc là null
{"items": []}
{"items": null}
Solution:
test('Handle inconsistent types', async ({ request }) => {
const response = await request.get('https://api.example.com/data');
const data = await response.json();
// Normalize id to number
const id = typeof data.id === 'string' ? parseInt(data.id) : data.id;
// Handle null/undefined arrays
const items = data.items || [];
expect(typeof id).toBe('number');
expect(Array.isArray(items)).toBeTruthy();
});
Problem: Error details nằm sâu trong response
{
"response": {
"error": {
"details": {
"message": "The actual error message"
}
}
}
}
Solution:
test('Extract nested error', async ({ request }) => {
const response = await request.post('https://api.example.com/endpoint', {
data: { invalid: 'data' }
});
const body = await response.json();
// Navigate nested structure safely
const errorMessage = body?.response?.error?.details?.message
|| body?.error?.message
|| body?.message
|| 'Unknown error';
expect(errorMessage).toBeDefined();
});
import { test, expect } from '@playwright/test';
async function parseResponse(response) {
const contentType = response.headers()['content-type'] || '';
// Empty body check
const text = await response.text();
if (!text) {
return { empty: true, status: response.status() };
}
// JSON response
if (contentType.includes('application/json')) {
try {
return JSON.parse(text);
} catch (e) {
return { parseError: true, raw: text };
}
}
// Non-JSON response
return { raw: text, contentType };
}
test('Robust response handling', async ({ request }) => {
const response = await request.get('https://api.example.com/users/1');
const result = await parseResponse(response);
if (result.empty) {
console.log('Empty response with status:', result.status);
return;
}
if (result.parseError) {
console.log('Failed to parse JSON:', result.raw);
return;
}
if (result.raw) {
console.log('Non-JSON response:', result.raw);
return;
}
// Valid JSON response
expect(result).toHaveProperty('id');
expect(result).toHaveProperty('name');
});