QA Engineer Skills 2026QA-2026HTTP Fundamentals

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

  1. Status code: Did the server accept/reject the request correctly?
  2. Response headers: Are security headers present? Is the content type correct?
  3. Response body: Does the data match expectations? Are all fields present?
  4. Response time: Is the response within acceptable latency?
  5. 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

  1. Use curl or requests to make GET, POST, PUT, and DELETE requests to a public API (e.g., JSONPlaceholder)
  2. Inspect response headers for each request and verify Content-Type, status codes, and any security headers
  3. Write a test that verifies proper 401 handling: send a request without auth, with expired auth, and with invalid auth
  4. 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