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/octocatmeans "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 toAccept— what response format you want ("give me JSON, not HTML")User-Agent— what software is making the requestAuthorization— authentication credentialsContent-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:
GETandDELETE→ usually no bodyPOST,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 bytesDate— when the response was generatedX-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
| Component | Request | Response |
|---|---|---|
| First line | GET /users/octocat HTTP/1.1 | HTTP/1.1 200 OK |
| Contains | Method + path + version | Version + status code + reason |
| Headers | What the client needs to send | What the server reports |
| Body | Data to send (POST/PUT) | Data returned (JSON, HTML) |
| Who sends it | Client | Server |
| Who receives it | Server | Client |
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:
- Builds a request (method GET, a GitHub URL, headers with a token)
- Sends it to the server
- Receives the response
- Reads the status code to know whether it succeeded
- Parses the JSON body to pull out the data
- 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)
GETusually carries no body;POSTandPUTdo- In Python,
requests.get()returns aResponseobject with all the information:.status_code,.headers,.json(),.text - The
response.requestobject 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
- MDN Web Docs: HTTP Messages - A visual anatomy of requests and responses, with diagrams
- httpbin.org - A service for testing HTTP requests (it mirrors back what you send)
- Requests: HTTP for Humans - The official quickstart for the requests library
- MDN: HTTP request methods - Reference for every HTTP method
- How HTTP Works (comic) - A visual, fun explanation of the protocol
- RFC 9110: HTTP Semantics - The official spec (a reference, not required reading)