Module 1: HTTP Protocol Fundamentals

Anatomy of HTTP: request and response

Capsule overview

HTTP (HyperText Transfer Protocol) is a communication protocol built on a simple model: a client sends a request and a server returns a response. That's it. Every interaction on the web — loading a page, submitting a form, calling an API — follows this cycle.

In this capsule you'll break both objects down into all their parts. You'll see exactly what a request contains (request line, headers, body) and what a response contains (status line, headers, body). By the end, you'll be able to read any HTTP request and understand what's going on.

This knowledge is the foundation of everything that follows: methods, status codes, headers — they all live inside the structure you'll learn here.


The client-server model

HTTP works with two actors:

  • Client: whoever starts the conversation. Your browser, a Python script, curl, a mobile app.
  • Server: whoever receives the request and returns a response. A web server, an API, a cloud service.
Client                               Server
(your Python code)                   (the GitHub API)
      │                                  │
      │── HTTP Request ────────────────→ │
      │   "Give me octocat's repos"      │
      │                                  │
      │←─ HTTP Response ────────────────│
      │   "Here are 30 repos"            │
      │                                  │

The client always starts. The server always responds. The server never sends data unless the client asks for it first (in classic HTTP).

Analogy: HTTP is like a restaurant. You (the client) ask the waiter (HTTP) for a dish. The waiter takes your order (the request) to the kitchen (the server). The kitchen prepares the food and the waiter brings it back to you (the response). The kitchen never sends you food you didn't order.


Anatomy of an HTTP request

An HTTP request has 3 parts:

┌─────────────────────────────────────────────┐
│ 1. REQUEST LINE                             │
│    GET /users/octocat HTTP/1.1              │
├─────────────────────────────────────────────┤
│ 2. HEADERS (metadata)                       │
│    Host: api.github.com                     │
│    Accept: application/json                 │
│    User-Agent: Python/3.11                  │
├─────────────────────────────────────────────┤
│ 3. BODY (data — optional)                   │
│    {"name": "New repo", "private": false}   │
└─────────────────────────────────────────────┘

Part 1: request line

The first line of every HTTP request has 3 components:

GET /users/octocat HTTP/1.1
│    │               │
│    │               └─ Protocol version
│    └─ Path (the resource you're asking for)
└─ Method (what you want to do)
  • Method: the operation you want to perform. GET = read, POST = create, PUT = replace, DELETE = remove. Methods are covered in depth in capsule 03.
  • Path: the address of the resource. /users/octocat means "the user octocat." Host + path together form the full URL: https://api.github.com/users/octocat.
  • Version: almost always HTTP/1.1. HTTP/2 and HTTP/3 exist, but the request/response structure is the same.

Part 2: headers

Headers are key: value pairs that carry metadata about the request. They're like the instructions written on the envelope of a letter.

Host: api.github.com
Accept: application/json
User-Agent: python-requests/2.31.0
Authorization: Bearer ghp_abc123xyz
Content-Type: application/json

Each header tells the server something:

  • Host — which server the request is addressed to
  • Accept — what response format you want ("give me JSON, not HTML")
  • User-Agent — what software is making the request
  • Authorization — authentication credentials
  • Content-Type — what kind of data you're sending in the body

Headers are covered in depth in capsule 05. For now, just understand that they're metadata riding along with every request.

Part 3: body

The body is the content of the request — the data you're sending to the server. Not every request has a body.

# GET: no body (you're only asking for data)
GET /users/octocat HTTP/1.1

# POST: with a body (you're sending data to create something)
POST /repos HTTP/1.1
Content-Type: application/json

{"name": "my-new-repo", "description": "A cool project", "private": false}

Rule of thumb:

  • GET and DELETE → usually no body
  • POST, PUT, PATCH → usually with a body

The body can be JSON, form data, plain text, or binary. In modern APIs, 95% of the time it's JSON.


Anatomy of an HTTP response

An HTTP response also has 3 parts:

┌─────────────────────────────────────────────┐
│ 1. STATUS LINE                              │
│    HTTP/1.1 200 OK                          │
├─────────────────────────────────────────────┤
│ 2. HEADERS (response metadata)              │
│    Content-Type: application/json           │
│    Content-Length: 1234                      │
│    X-RateLimit-Remaining: 58                │
├─────────────────────────────────────────────┤
│ 3. BODY (the data returned)                 │
│    {"login": "octocat", "id": 583231, ...}  │
└─────────────────────────────────────────────┘

Part 1: status line

The first line of the response tells you whether the request succeeded or not:

HTTP/1.1 200 OK
│        │   │
│        │   └─ Reason phrase (descriptive text)
│        └─ Status code (a number)
└─ Protocol version
  • Status code: a 3-digit number telling you the outcome. 200 = success, 404 = not found, 500 = server error. Status codes are covered in capsule 04.
  • Reason phrase: descriptive text for the status. OK, Not Found, Internal Server Error. It's informational — the numeric code is what matters.

Part 2: response headers

Response headers are metadata the server sends back:

Content-Type: application/json; charset=utf-8
Content-Length: 1354
Date: Thu, 13 Mar 2026 10:30:00 GMT
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
Cache-Control: public, max-age=60

Typical information in response headers:

  • Content-Type — the format of the data returned (JSON, HTML, text)
  • Content-Length — the size of the response in bytes
  • Date — when the response was generated
  • X-RateLimit-* — how many requests you have left (rate limiting)
  • Cache-Control — how long you can cache the response

Part 3: response body

The body holds the data you asked for (or an error message):

{
  "login": "octocat",
  "id": 583231,
  "name": "The Octocat",
  "company": "@github",
  "location": "San Francisco",
  "public_repos": 8,
  "followers": 12345
}

If there's an error, the body usually contains an explanatory message:

{
  "message": "Not Found",
  "documentation_url": "https://docs.github.com/rest"
}

Seeing it in action with Python

Let's look at a real request and response using Python:

import requests

# Make a GET request to the GitHub API
response = requests.get("https://api.github.com/users/octocat")

# --- The REQUEST that was sent ---
print("=== REQUEST ===")
print(f"Method: GET")
print(f"URL: https://api.github.com/users/octocat")
print(f"Headers sent:")
for key, value in response.request.headers.items():
    print(f"  {key}: {value}")

print()

# --- The RESPONSE we got back ---
print("=== RESPONSE ===")
print(f"Status code: {response.status_code}")
print(f"Reason: {response.reason}")
print(f"Headers received:")
for key, value in response.headers.items():
    print(f"  {key}: {value}")

print()
print(f"Body (first 200 characters):")
print(response.text[:200])

Expected output:

=== REQUEST ===
Method: GET
URL: https://api.github.com/users/octocat
Headers sent:
  User-Agent: python-requests/2.31.0
  Accept-Encoding: gzip, deflate
  Accept: */*
  Connection: keep-alive

=== RESPONSE ===
Status code: 200
Reason: OK
Headers received:
  Content-Type: application/json; charset=utf-8
  X-RateLimit-Limit: 60
  X-RateLimit-Remaining: 59
  ...

Body (first 200 characters):
{"login":"octocat","id":583231,"node_id":"MDQ6VXNlcjU4MzIzMQ==",...

This example shows the 3 parts of the request (method, URL, headers) and the 3 parts of the response (status, headers, body) with real data.


Request and response as Python objects

When you use requests.get(), Python hands you back a Response object that contains everything the server replied with:

import requests

response = requests.get("https://api.github.com/users/octocat")

# The response object holds all the information
print(type(response))            # <class 'requests.models.Response'>

# Status
print(response.status_code)      # 200
print(response.reason)           # OK
print(response.ok)               # True (status < 400)

# Headers (like a dictionary)
print(response.headers["Content-Type"])  # application/json; charset=utf-8

# Body
print(response.text)             # String with the full body
print(response.json())           # Python dict (if the body is JSON)

# Info about the original request
print(response.request.method)   # GET
print(response.request.url)      # https://api.github.com/users/octocat
print(response.request.headers)  # The headers that were sent

Expected output:

<class 'requests.models.Response'>
200
OK
True
application/json; charset=utf-8
{"login":"octocat","id":583231,...}
{'login': 'octocat', 'id': 583231, ...}
GET
https://api.github.com/users/octocat
{'User-Agent': 'python-requests/2.31.0', ...}

The response object is your full window into what happened: what you sent and what you got back.


The full cycle: step by step

Let's see what happens when your code runs requests.get("https://api.github.com/users/octocat"):

1. Your code calls requests.get()
     ↓
2. requests builds the HTTP request:
     GET /users/octocat HTTP/1.1
     Host: api.github.com
     User-Agent: python-requests/2.31.0
     Accept: */*
     ↓
3. The request travels over the network to GitHub's server
     ↓
4. GitHub's server:
     a. Receives the request
     b. Reads the method (GET) and the path (/users/octocat)
     c. Looks up the user "octocat" in its database
     d. Builds the response
     ↓
5. The server sends the response:
     HTTP/1.1 200 OK
     Content-Type: application/json
     
     {"login": "octocat", "id": 583231, ...}
     ↓
6. The response travels back over the network
     ↓
7. requests receives the response and hands it to you as a Python object
     ↓
8. Your code reads response.status_code, response.json(), etc.

All of this happens in milliseconds. Every time you make an HTTP request, this full cycle runs.


Comparison: request vs response

ComponentRequestResponse
First lineGET /users/octocat HTTP/1.1HTTP/1.1 200 OK
ContainsMethod + path + versionVersion + status code + reason
HeadersWhat the client needs to sendWhat the server reports
BodyData to send (POST/PUT)Data returned (JSON, HTML)
Who sends itClientServer
Who receives itServerClient

The key point: both have the same structure (first line + headers + body), but with opposite purposes.


Connection to the project

In the REST Client CLI (Module 4), every command you implement runs this request/response cycle. When the user types python cli.py github user octocat, your code:

  1. Builds a request (method GET, a GitHub URL, headers with a token)
  2. Sends it to the server
  3. Receives the response
  4. Reads the status code to know whether it succeeded
  5. Parses the JSON body to pull out the data
  6. Shows the data to the user

Without understanding the anatomy of request/response, you can't diagnose why a request fails or handle the different kinds of responses.


Troubleshooting

Problem 1: requests isn't installed

Error: ModuleNotFoundError: No module named 'requests'

Cause: You didn't install the library.

Fix:

pip install requests

If you're using a virtual environment, make sure it's activated before installing.

Problem 2: connection error

Error: requests.exceptions.ConnectionError: Failed to establish a new connection

Cause: You have no internet connection, or the URL is wrong.

Fix:

# Check the URL
response = requests.get("https://api.github.com/users/octocat")
# ✅ Correct URL with https://

response = requests.get("api.github.com/users/octocat")
# ❌ The protocol is missing (https://)

Problem 3: the response isn't JSON

Error: json.decoder.JSONDecodeError: Expecting value

Cause: You're trying to parse as JSON a response that isn't JSON (an HTML error page, for instance).

Fix:

# Always check the Content-Type before parsing
print(response.headers.get("Content-Type"))

# Or check the status code first
if response.ok:
    data = response.json()
else:
    print(f"Error: {response.status_code} - {response.text[:100]}")

Exercises

Exercise 1: Inspect a response (Easy)

Make a GET request to https://api.github.com (the root of the GitHub API) and print:

  • The status code
  • The response's Content-Type
  • The first 5 keys of the JSON returned
See solution
import requests

response = requests.get("https://api.github.com")

print(f"Status code: {response.status_code}")
print(f"Content-Type: {response.headers['Content-Type']}")

data = response.json()
first_5_keys = list(data.keys())[:5]
print(f"First 5 keys: {first_5_keys}")

Expected output:

Status code: 200
Content-Type: application/json; charset=utf-8
First 5 keys: ['current_user_url', 'current_user_authorizations_html_url', 'authorizations_url', 'code_search_url', 'commit_search_url']

Explanation: response.json() turns the JSON body into a Python dictionary. .keys() gives you all the dictionary's keys.

Exercise 2: Count the response headers (Easy)

Make a GET request to https://httpbin.org/get and show how many headers the response returns and the name of each one.

See solution
import requests

response = requests.get("https://httpbin.org/get")

print(f"Number of headers: {len(response.headers)}")
print("\nResponse headers:")
for header_name in response.headers:
    print(f"  - {header_name}")

Expected output:

Number of headers: 8
Response headers:
  - Date
  - Content-Type
  - Content-Length
  - Connection
  - Server
  - Access-Control-Allow-Origin
  - Access-Control-Allow-Credentials
  - ...

Explanation: response.headers behaves like a dictionary. You can iterate over it to see every header the server sent.

Exercise 3: Compare request vs response headers (Medium)

Make a GET request to https://httpbin.org/get. httpbin returns in its JSON body the headers that YOU sent. Compare the headers you sent (from the response body) with the headers the server sent back (from response.headers).

See solution
import requests

response = requests.get("https://httpbin.org/get")
data = response.json()

print("=== HEADERS YOU SENT ===")
for key, value in data["headers"].items():
    print(f"  {key}: {value}")

print()
print("=== HEADERS THE SERVER SENT BACK ===")
for key, value in response.headers.items():
    print(f"  {key}: {value}")

print()
print(f"Headers sent: {len(data['headers'])}")
print(f"Headers received: {len(response.headers)}")

Explanation: httpbin.org is a service that mirrors back whatever you send it. The headers field in its JSON response contains your request's headers. response.headers are the headers from the server's response. They're two distinct sets — one is what you send, the other is what the server replies with.

Exercise 4: Full anatomy of a request (Medium)

Write a function print_request_anatomy(url) that takes a URL, makes a GET request, and shows the full anatomy: request line, request headers, status line, response headers, and the first 100 characters of the body.

See solution
import requests

def print_request_anatomy(url: str) -> None:
    """Shows the full anatomy of an HTTP request/response cycle."""
    response = requests.get(url)

    print("=" * 50)
    print("REQUEST")
    print("=" * 50)
    print(f"  {response.request.method} {response.request.path_url} HTTP/1.1")
    print(f"  Headers:")
    for key, value in response.request.headers.items():
        print(f"    {key}: {value}")

    print()
    print("=" * 50)
    print("RESPONSE")
    print("=" * 50)
    print(f"  HTTP/1.1 {response.status_code} {response.reason}")
    print(f"  Headers:")
    for key, value in response.headers.items():
        print(f"    {key}: {value}")

    print()
    print(f"  Body (first 100 chars):")
    print(f"    {response.text[:100]}...")

# Try it with different URLs
print_request_anatomy("https://api.github.com/users/octocat")
print()
print_request_anatomy("https://httpbin.org/get")

Explanation: response.request gives you access to the original request. .path_url returns the path without the host. This function is a simplified version of the HTTP Explorer project you'll build in capsule 08.

Exercise 5: Detect the content type (Hard)

Write a function that takes a URL, makes a GET, and determines whether the response is JSON, HTML, or plain text based on the Content-Type header. If it's JSON, show the top-level keys. If it's HTML, show the first 150 characters. If it's text, show all of it.

See solution
import requests

def analyze_response_content(url: str) -> None:
    """Analyzes the content type of an HTTP response."""
    try:
        response = requests.get(url, timeout=10)
    except requests.exceptions.RequestException as e:
        print(f"Error making the request: {e}")
        return

    content_type = response.headers.get("Content-Type", "unknown")
    print(f"URL: {url}")
    print(f"Status: {response.status_code}")
    print(f"Content-Type: {content_type}")
    print()

    if "application/json" in content_type:
        print("Type: JSON")
        data = response.json()
        if isinstance(data, dict):
            print(f"Top-level keys: {list(data.keys())}")
        elif isinstance(data, list):
            print(f"Array with {len(data)} elements")
    elif "text/html" in content_type:
        print("Type: HTML")
        print(f"First 150 chars: {response.text[:150]}...")
    else:
        print(f"Type: {content_type}")
        print(f"Content: {response.text[:200]}")

# Try it with different content types
analyze_response_content("https://api.github.com/users/octocat")
analyze_response_content("https://httpbin.org/html")
analyze_response_content("https://httpbin.org/robots.txt")

Explanation: The Content-Type header tells you what format the body is in. Checking it before parsing avoids errors like calling .json() on an HTML response. In the REST Client CLI, you'll need this logic to handle different APIs.


Summary

In this capsule you learned:

  • HTTP follows the client-server model: the client sends a request, the server returns a response
  • An HTTP request has 3 parts: the request line (method + path + version), headers (metadata), and a body (optional data)
  • An HTTP response has 3 parts: the status line (version + status code + reason), headers (metadata), and a body (the data returned)
  • GET usually carries no body; POST and PUT do
  • In Python, requests.get() returns a Response object with all the information: .status_code, .headers, .json(), .text
  • The response.request object gives you access to the original request that was sent

Next capsule: HTTP methods — you'll go deeper into GET, POST, PUT, DELETE, and PATCH to understand when to use each one.


Additional resources

  1. MDN Web Docs: HTTP Messages - A visual anatomy of requests and responses, with diagrams
  2. httpbin.org - A service for testing HTTP requests (it mirrors back what you send)
  3. Requests: HTTP for Humans - The official quickstart for the requests library
  4. MDN: HTTP request methods - Reference for every HTTP method
  5. How HTTP Works (comic) - A visual, fun explanation of the protocol
  6. RFC 9110: HTTP Semantics - The official spec (a reference, not required reading)