HTTP Fundamentals
Every API test, every browser automation step, and every network-level debugging session depends on understanding HTTP. This is not theory — it is the protocol that your tests speak every time they interact with a web application.
HTTP Methods
| Method |
Purpose |
Idempotent? |
Safe? |
| GET |
Retrieve resource |
Yes |
Yes |
| POST |
Create resource |
No |
No |
| PUT |
Replace resource entirely |
Yes |
No |
| PATCH |
Partial update |
No |
No |
| DELETE |
Remove resource |
Yes |
No |
| HEAD |
Same as GET but no body |
Yes |
Yes |
| OPTIONS |
Discover allowed methods |
Yes |
Yes |
What Idempotent Means for Testing
An idempotent request produces the same result whether you send it once or ten times. This matters for retry logic and test reliability:
PUT /users/123 {"name": "Alice"} — send it 10 times, the user is still named "Alice"
POST /users {"name": "Alice"} — send it 10 times, you may get 10 users (or 409 Conflict if email is unique)
DELETE /users/123 — first call deletes the user, subsequent calls return 404 (but the state is the same: user is deleted)
What to Test for Each Method
# GET: verify response content and caching
def test_get_user(api):
r = api.get("/users/1")
assert r.status_code == 200
assert r.headers["Content-Type"] == "application/json"
assert "id" in r.json()
# POST: verify creation and response
def test_create_user(api):
r = api.post("/users", json={"name": "Alice", "email": "alice@test.com"})
assert r.status_code == 201
assert "id" in r.json()
# Verify the user actually exists
get_r = api.get(f"/users/{r.json()['id']}")
assert get_r.status_code == 200
# PUT: verify full replacement
def test_update_user(api):
r = api.put("/users/1", json={"name": "Bob", "email": "bob@test.com"})
assert r.status_code == 200
# Verify ALL fields are what you sent (PUT replaces everything)
user = api.get("/users/1").json()
assert user["name"] == "Bob"
# DELETE: verify removal
def test_delete_user(api):
r = api.delete("/users/1")
assert r.status_code == 204
# Verify the user is gone
get_r = api.get("/users/1")
assert get_r.status_code == 404
HTTP Status Codes
| Range |
Meaning |
Common Codes |
| 2xx |
Success |
200 OK, 201 Created, 204 No Content |
| 3xx |
Redirection |
301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx |
Client error |
400, 401, 403, 404, 409, 422, 429 |
| 5xx |
Server error |
500, 502, 503 |
Status Codes QA Engineers Must Know
| Code |
Meaning |
Test Scenario |
| 200 |
OK |
Successful GET, PUT, PATCH |
| 201 |
Created |
Successful POST |
| 204 |
No Content |
Successful DELETE |
| 301 |
Moved Permanently |
Old URL should redirect to new URL |
| 304 |
Not Modified |
Caching: resource has not changed since last request |
| 400 |
Bad Request |
Malformed JSON, missing required fields |
| 401 |
Unauthorized |
Missing or expired auth token |
| 403 |
Forbidden |
Valid token, but insufficient permissions |
| 404 |
Not Found |
Non-existent resource |
| 405 |
Method Not Allowed |
POST to a GET-only endpoint |
| 409 |
Conflict |
Duplicate creation (unique constraint) |
| 422 |
Unprocessable Entity |
Valid JSON but invalid data (e.g., email format) |
| 429 |
Too Many Requests |
Rate limit exceeded |
| 500 |
Internal Server Error |
Unhandled exception on the server |
| 502 |
Bad Gateway |
Backend service is down |
| 503 |
Service Unavailable |
Server overloaded or under maintenance |
HTTP Headers
Headers carry metadata about the request and response. Several headers are critical for testing.
Request Headers to Set
| Header |
Purpose |
Example |
Content-Type |
Format of request body |
application/json |
Authorization |
Authentication credential |
Bearer eyJhbG... |
Accept |
Desired response format |
application/json |
User-Agent |
Client identification |
Custom value for test identification |
Response Headers to Verify
| Header |
What to Test |
Content-Type |
Matches expected format (wrong type causes silent failures) |
Set-Cookie |
Verify HttpOnly, Secure, SameSite attributes |
Access-Control-Allow-Origin |
CORS configuration correct for allowed domains |
Cache-Control |
Appropriate caching for the resource type |
X-Request-ID |
Present for traceability (useful for debugging) |
Retry-After |
Present on 429 responses |
Location |
Present on 201 (points to new resource) and 3xx redirects |
def test_security_headers(api):
r = api.get("/")
# Security headers should be present
assert "X-Content-Type-Options" in r.headers
assert r.headers["X-Content-Type-Options"] == "nosniff"
assert "X-Frame-Options" in r.headers
assert "Strict-Transport-Security" in r.headers
def test_cors_headers(api):
r = api.options("/api/users", headers={
"Origin": "https://app.example.com",
"Access-Control-Request-Method": "GET"
})
assert r.headers["Access-Control-Allow-Origin"] in [
"https://app.example.com", "*"
]
The Request/Response Cycle
Client Server
| |
|-- HTTP Request ---------------------->|
| Method: POST |
| URL: /api/users |
| Headers: |
| Content-Type: application/json |
| Authorization: Bearer token |
| Body: {"name": "Alice"} |
| |
|<-- HTTP Response --------------------|
| Status: 201 Created |
| Headers: |
| Content-Type: application/json |
| Location: /api/users/42 |
| Body: {"id": 42, "name": "Alice"} |
What to Verify at Each Layer
- Status code: Did the server accept/reject the request correctly?
- Response headers: Are security headers present? Is the content type correct?
- Response body: Does the data match expectations? Are all fields present?
- Response time: Is the response within acceptable latency?
- Side effects: Did the server actually create/update/delete the resource?
Query Parameters vs Request Body
| Aspect |
Query Parameters |
Request Body |
| Used with |
GET, DELETE |
POST, PUT, PATCH |
| Visible in URL |
Yes |
No |
| Cacheable |
Yes (part of URL) |
No |
| Size limit |
~2KB (URL length limit) |
No practical limit |
| Example |
GET /users?page=1&limit=10 |
POST /users {"name": "Alice"} |
# Query parameters
r = requests.get(f"{base_url}/users", params={"page": 1, "limit": 10, "sort": "name"})
# Results in: GET /users?page=1&limit=10&sort=name
# Request body
r = requests.post(f"{base_url}/users", json={"name": "Alice", "email": "alice@test.com"})
Content Types
| Content-Type |
Used For |
Example |
application/json |
REST APIs |
{"name": "Alice"} |
application/x-www-form-urlencoded |
HTML forms |
name=Alice&email=alice@test.com |
multipart/form-data |
File uploads |
Binary file data with boundaries |
text/html |
Web pages |
HTML content |
application/xml |
SOAP APIs, legacy systems |
<user><name>Alice</name></user> |
# Sending form data (not JSON)
r = requests.post(f"{base_url}/login", data={"username": "alice", "password": "pass123"})
# Uploading a file
with open("document.pdf", "rb") as f:
r = requests.post(f"{base_url}/upload", files={"file": f})
Practical Exercise
- Use curl or requests to make GET, POST, PUT, and DELETE requests to a public API (e.g., JSONPlaceholder)
- Inspect response headers for each request and verify Content-Type, status codes, and any security headers
- Write a test that verifies proper 401 handling: send a request without auth, with expired auth, and with invalid auth
- Write a test that verifies proper CORS headers for a cross-origin request
Key Takeaways
- Know all HTTP methods and their idempotency characteristics
- Test both success and error status codes (especially 400, 401, 403, 404, 409, 429)
- Verify response headers, not just body — security headers, CORS, caching
- Understand the difference between query parameters (GET) and request body (POST/PUT)
- Content-Type mismatches cause silent failures — always verify