Module 1: HTTP Protocol Fundamentals
Module 1 project: HTTP Explorer
Project overview
Across this module you learned what an HTTP request is, what a response is, how methods work, what status codes mean, what headers are for, and how to make requests with Python and from the terminal. Each capsule covered one piece of the puzzle. Now you're going to build something that pulls all those pieces into a single tool.
HTTP Explorer is a Python command-line script that takes a URL (or several), makes HTTP requests, and shows the full anatomy of each transaction: the request line that was sent, the headers that traveled, the status code that came back, the response headers, a preview of the body, how long it took, and an automatic diagnosis of the result. It's like having the browser's DevTools, but in your terminal and under your control.
Why does this matter? Because in your career as a backend developer you'll consume hundreds of APIs. When something fails — and it will — you need a mental (and practical) tool to break the HTTP transaction apart and find the problem. Did the server return a 401? Your Authorization header is wrong. A 500? The problem is on the server. A timeout? The connection isn't being established. HTTP Explorer trains you to think that way.
This is the Module 1 project. It's a contained project that proves you've got the fundamentals of the HTTP protocol down. The guide's final project — the REST Client CLI in Module 4 — is bigger and consumes multiple public APIs. What you build here is the foundation that project rests on.
Project objective
Build a Python command-line script that analyzes and displays the full anatomy of any HTTP transaction.
By the end of this project, you'll be able to:
- ✅ Make HTTP requests from a Python script with
requests - ✅ Inspect every component of a request: method, URL, headers sent
- ✅ Inspect every component of a response: status code, reason, headers, body
- ✅ Diagnose the result of a request based on the status code and the headers
- ✅ Measure a request's response time
- ✅ Compare responses from multiple URLs
- ✅ Handle network errors robustly
Estimated time: 45-60 minutes.
Technical specifications
Stack
- Python 3.8+
- requests — the HTTP library for Python
- argparse — the standard module for parsing command-line arguments (bundled with Python)
Setup
# Check your Python version
python --version
# Python 3.8+ required
# If you don't have the module's directory yet, create it
mkdir -p rest-apis-guide/module-01
cd rest-apis-guide/module-01
# Install requests (if you didn't do it in capsule 01)
pip install requests
You don't need any other dependency. argparse and json are bundled with Python's standard library.
Required features
Feature 1: URL analysis
The script takes a URL as a command-line argument, makes a GET request, and shows the full anatomy of the HTTP transaction.
python http_explorer.py https://api.github.com/users/octocat
It has to show: the request line, the request headers, the response status, the response headers, a body preview, and the timing.
Feature 2: Request anatomy
Shows every component of the request that was sent:
- 🔹 The HTTP method used (GET, POST, etc.)
- 🔹 The full URL
- 🔹 Every header
requestssent automatically (Host, User-Agent, Accept, etc.)
The user should be able to see exactly what left their machine on its way to the server.
Feature 3: Response anatomy
Shows every component of the response the server returned:
- 🔹 The status code and reason phrase (e.g.
200 OK,404 Not Found) - 🔹 Every response header (Content-Type, Server, Date, etc.)
- 🔹 A preview of the body (the first 500 characters)
- 🔹 The total body size in bytes
Feature 4: Diagnostics
Analyzes the response and shows an automatic diagnosis:
- 🔹 The status category: Success (2xx), Redirect (3xx), Client Error (4xx), Server Error (5xx)
- 🔹 The response time in milliseconds
- 🔹 The Content-Type detected (JSON, HTML, XML, text, other)
- 🔹 If it's JSON, it says the body is parseable; if not, it reports the actual type
Feature 5: Multiple URLs
Accepts multiple URLs and shows the anatomy of each one, with a comparative summary at the end.
python http_explorer.py https://api.github.com/users/octocat https://httpbin.org/get
The summary shows: the URL, the status code, the response time, and the Content-Type of each request.
Feature 6: Custom method support
Lets you specify the HTTP method and an optional body via command-line arguments.
python http_explorer.py https://httpbin.org/post --method POST --body '{"test": true}'
Supports GET, POST, PUT, PATCH, DELETE. If a body is sent, it automatically adds the Content-Type: application/json header.
Validation and error handling
Your script must handle these scenarios without crashing:
| Scenario | What should happen |
|---|---|
Invalid URL (e.g. not-a-url) | Show a clear error: "Invalid URL" along with the URL that failed |
Connection error (e.g. https://this-does-not-exist.xyz) | Show "Connection error" with no Python traceback |
| Timeout (the server doesn't answer in 10s) | Show "Timeout: the server didn't answer in 10 seconds" |
| Response with no JSON (e.g. an HTML page) | Show the body preview as text, don't try to parse JSON |
| An error status code (4xx, 5xx) | Do NOT treat it as a Python error — show the anatomy as usual, with a diagnosis |
The rule is simple: the script must never show a Python traceback to the user. Every error gets caught and shown as a readable message.
Success criteria
Your HTTP Explorer is complete if:
- ✅ It accepts one or more URLs as command-line arguments
- ✅ It shows the method, URL, and headers of the request that was sent
- ✅ It shows the status code, reason, and headers of the response that came back
- ✅ It shows a preview of the body (the first 500 characters)
- ✅ It shows the response time in milliseconds
- ✅ It categorizes the status code (success, redirect, client error, server error)
- ✅ It supports --method to change the HTTP method
- ✅ It supports --body to send data in the request
- ✅ It handles errors (invalid URL, failed connection, timeout) without crashing
- ✅ It shows a comparative summary when several URLs are passed
Guided step by step
Step 1: The base structure and argparse
Start by creating the skeleton of the script with argparse to handle the command-line arguments.
import argparse
import requests
import json
import sys
def parse_arguments():
parser = argparse.ArgumentParser(
description="HTTP Explorer — Analyzes the anatomy of HTTP transactions"
)
parser.add_argument(
"urls",
nargs="+",
help="One or more URLs to analyze"
)
parser.add_argument(
"--method",
default="GET",
choices=["GET", "POST", "PUT", "PATCH", "DELETE"],
help="The HTTP method to use (default: GET)"
)
parser.add_argument(
"--body",
default=None,
help="The request body in JSON format (e.g. '{\"key\": \"value\"}')"
)
return parser.parse_args()
argparse gives you validation for free: if the user passes a method that isn't in choices, it shows an error automatically. nargs="+" means "one or more URLs."
Step 2: A function to make the request
Create a function that makes the request and catches every possible error. This function never raises exceptions outward — it always returns either a result or a readable error.
def make_request(url, method="GET", body=None):
headers = {}
data = None
if body:
try:
data = json.loads(body)
headers["Content-Type"] = "application/json"
except json.JSONDecodeError:
return None, f"The body isn't valid JSON: {body}"
try:
response = requests.request(
method=method,
url=url,
json=data if data else None,
headers=headers if not data else None,
timeout=10
)
return response, None
except requests.exceptions.MissingSchema:
return None, f"Invalid URL (http:// or https:// is missing): {url}"
except requests.exceptions.ConnectionError:
return None, f"Connection error: couldn't connect to {url}"
except requests.exceptions.Timeout:
return None, f"Timeout: the server didn't answer in 10 seconds"
except requests.exceptions.RequestException as e:
return None, f"Error in the request: {e}"
Notice the pattern: the function returns a tuple (response, error). If error isn't None, something went wrong. If response isn't None, the request succeeded (including 4xx and 5xx — those aren't network errors, they're valid responses from the server).
Step 3: Show the request's anatomy
Create a function that shows what was sent to the server. The requests library gives you access to the prepared request through response.request.
def display_request_anatomy(response):
req = response.request
print("=" * 65)
print(" REQUEST ANATOMY")
print("=" * 65)
print(f" Method: {req.method}")
print(f" URL: {req.url}")
print("-" * 65)
print(" Headers sent:")
for key, value in req.headers.items():
print(f" {key}: {value}")
if req.body:
print("-" * 65)
print(f" Body sent:")
body_str = req.body if isinstance(req.body, str) else req.body.decode("utf-8")
print(f" {body_str}")
print()
response.request is a PreparedRequest object holding exactly what requests sent over the network. The headers include the ones requests adds automatically: User-Agent, Accept-Encoding, Accept, Connection.
Step 4: Show the response's anatomy
Now the server's side: what it sent you back.
def display_response_anatomy(response):
print("=" * 65)
print(" RESPONSE ANATOMY")
print("=" * 65)
print(f" Status: {response.status_code} {response.reason}")
print(f" Size: {len(response.content)} bytes")
print("-" * 65)
print(" Headers received:")
for key, value in response.headers.items():
print(f" {key}: {value}")
print("-" * 65)
print(" Body preview (first 500 chars):")
body_preview = response.text[:500]
content_type = response.headers.get("Content-Type", "")
if "json" in content_type:
try:
parsed = json.loads(response.text)
body_preview = json.dumps(parsed, indent=2)[:500]
except json.JSONDecodeError:
pass
print(f" {body_preview}")
if len(response.text) > 500:
print(f" ... ({len(response.text) - 500} more chars)")
print()
If the Content-Type says JSON, it tries to format it with json.dumps(indent=2) so it's readable. If parsing fails, it shows the raw text. That covers the case of a server that claims application/json but returns something broken.
Step 5: Automatic diagnostics
The diagnostics function categorizes the result and gives useful context.
def display_diagnostics(response):
status = response.status_code
elapsed_ms = response.elapsed.total_seconds() * 1000
content_type = response.headers.get("Content-Type", "unknown")
if 200 <= status < 300:
category = "SUCCESS (2xx)"
elif 300 <= status < 400:
category = "REDIRECT (3xx)"
elif 400 <= status < 500:
category = "CLIENT ERROR (4xx)"
elif 500 <= status < 600:
category = "SERVER ERROR (5xx)"
else:
category = "UNKNOWN"
content_label = "Other"
if "json" in content_type:
content_label = "JSON (parseable)"
elif "html" in content_type:
content_label = "HTML"
elif "xml" in content_type:
content_label = "XML"
elif "text" in content_type:
content_label = "Plain text"
print("=" * 65)
print(" DIAGNOSTICS")
print("=" * 65)
print(f" Status category: {category}")
print(f" Response time: {elapsed_ms:.0f} ms")
print(f" Content-Type: {content_type}")
print(f" Content format: {content_label}")
print()
response.elapsed is a timedelta that requests computes automatically — it measures the time from when the request was sent to when the first part of the response arrived. It's the most precise way to measure latency without adding your own time.time().
Step 6: A comparative summary for multiple URLs
When the user passes more than one URL, show a summary table at the end.
def display_summary(results):
if len(results) <= 1:
return
print("=" * 65)
print(" SUMMARY — Response comparison")
print("=" * 65)
print(f" {'URL':<35} {'Status':<10} {'Time':>8} {'Type':<15}")
print(f" {'-'*35} {'-'*10} {'-'*8} {'-'*15}")
for result in results:
url_short = result["url"]
if len(url_short) > 33:
url_short = url_short[:30] + "..."
print(
f" {url_short:<35} "
f"{result['status']:<10} "
f"{result['time']:>6.0f}ms "
f"{result['content_type']:<15}"
)
print()
Step 7: The main function that wires it all together
Now connect all the pieces in the main function.
def explore_url(url, method="GET", body=None):
print()
print("*" * 65)
print(f" Exploring: {url}")
print(f" Method: {method}")
print("*" * 65)
response, error = make_request(url, method, body)
if error:
print(f"\n ERROR: {error}\n")
return None
display_request_anatomy(response)
display_response_anatomy(response)
display_diagnostics(response)
content_type = response.headers.get("Content-Type", "unknown")
content_label = "JSON" if "json" in content_type else content_type.split(";")[0]
return {
"url": url,
"status": f"{response.status_code} {response.reason}",
"time": response.elapsed.total_seconds() * 1000,
"content_type": content_label
}
def main():
args = parse_arguments()
print()
print("+" * 65)
print(" HTTP EXPLORER v1.0")
print(" Analyzes the anatomy of HTTP transactions")
print("+" * 65)
results = []
for url in args.urls:
result = explore_url(url, args.method, args.body)
if result:
results.append(result)
display_summary(results)
print("+" * 65)
print(f" Exploration complete: {len(results)}/{len(args.urls)} URLs succeeded")
print("+" * 65)
print()
if __name__ == "__main__":
main()
The structure is clean: main parses the arguments, iterates over the URLs, and calls explore_url for each one. The successful results pile up for the final summary.
The complete, commented code
Here's the full script. Copy all of it into a file called http_explorer.py:
"""
HTTP Explorer v1.0
A script that analyzes the full anatomy of HTTP transactions.
Usage:
python http_explorer.py <url> [url2 url3 ...]
python http_explorer.py <url> --method POST --body '{"key": "value"}'
Module 1 — REST APIs & HTTP Fundamentals Guide
"""
import argparse
import json
import sys
import requests
def parse_arguments():
"""Sets up and parses the command-line arguments."""
parser = argparse.ArgumentParser(
description="HTTP Explorer — Analyzes the anatomy of HTTP transactions"
)
parser.add_argument(
"urls",
nargs="+",
help="One or more URLs to analyze"
)
parser.add_argument(
"--method",
default="GET",
choices=["GET", "POST", "PUT", "PATCH", "DELETE"],
help="The HTTP method to use (default: GET)"
)
parser.add_argument(
"--body",
default=None,
help="The request body in JSON format (e.g. '{\"key\": \"value\"}')"
)
return parser.parse_args()
def make_request(url, method="GET", body=None):
"""
Runs an HTTP request and returns (response, error).
If error isn't None, the request failed.
4xx/5xx statuses are NOT errors — they're valid responses.
"""
data = None
if body:
try:
data = json.loads(body)
except json.JSONDecodeError:
return None, f"The body isn't valid JSON: {body}"
try:
response = requests.request(
method=method,
url=url,
json=data,
timeout=10
)
return response, None
except requests.exceptions.MissingSchema:
return None, f"Invalid URL (http:// or https:// is missing): {url}"
except requests.exceptions.InvalidURL:
return None, f"Badly formatted URL: {url}"
except requests.exceptions.ConnectionError:
return None, f"Connection error: couldn't connect to {url}"
except requests.exceptions.Timeout:
return None, f"Timeout: the server didn't answer in 10 seconds"
except requests.exceptions.RequestException as e:
return None, f"Error in the request: {e}"
def display_request_anatomy(response):
"""Shows the components of the HTTP request that was sent."""
req = response.request
print("=" * 65)
print(" REQUEST ANATOMY")
print("=" * 65)
print(f" Method: {req.method}")
print(f" URL: {req.url}")
print("-" * 65)
print(" Headers sent:")
for key, value in req.headers.items():
print(f" {key}: {value}")
if req.body:
print("-" * 65)
print(" Body sent:")
body_str = req.body if isinstance(req.body, str) else req.body.decode("utf-8")
try:
formatted = json.dumps(json.loads(body_str), indent=2)
for line in formatted.split("\n"):
print(f" {line}")
except (json.JSONDecodeError, TypeError):
print(f" {body_str}")
print()
def display_response_anatomy(response):
"""Shows the components of the HTTP response that came back."""
print("=" * 65)
print(" RESPONSE ANATOMY")
print("=" * 65)
print(f" Status: {response.status_code} {response.reason}")
print(f" Size: {len(response.content)} bytes")
print("-" * 65)
print(" Headers received:")
for key, value in response.headers.items():
print(f" {key}: {value}")
print("-" * 65)
print(" Body preview (first 500 chars):")
content_type = response.headers.get("Content-Type", "")
body_preview = response.text[:500]
if "json" in content_type:
try:
parsed = json.loads(response.text)
body_preview = json.dumps(parsed, indent=2)[:500]
except json.JSONDecodeError:
pass
for line in body_preview.split("\n"):
print(f" {line}")
if len(response.text) > 500:
print(f" ... ({len(response.text) - 500} more chars)")
print()
def get_status_category(status_code):
"""Categorizes an HTTP status code."""
if 200 <= status_code < 300:
return "SUCCESS (2xx)"
elif 300 <= status_code < 400:
return "REDIRECT (3xx)"
elif 400 <= status_code < 500:
return "CLIENT ERROR (4xx)"
elif 500 <= status_code < 600:
return "SERVER ERROR (5xx)"
return "UNKNOWN"
def get_content_label(content_type):
"""Returns a readable label for the Content-Type."""
if "json" in content_type:
return "JSON (parseable)"
elif "html" in content_type:
return "HTML"
elif "xml" in content_type:
return "XML"
elif "text" in content_type:
return "Plain text"
return "Other"
def display_diagnostics(response):
"""Shows an automatic diagnosis of the response."""
status = response.status_code
elapsed_ms = response.elapsed.total_seconds() * 1000
content_type = response.headers.get("Content-Type", "unknown")
category = get_status_category(status)
content_label = get_content_label(content_type)
print("=" * 65)
print(" DIAGNOSTICS")
print("=" * 65)
print(f" Status category: {category}")
print(f" Response time: {elapsed_ms:.0f} ms")
print(f" Content-Type: {content_type}")
print(f" Content format: {content_label}")
if 400 <= status < 500:
print()
print(" Note: a 4xx error points to a problem in YOUR request.")
print(" Check the URL, the headers, or the body you sent.")
elif 500 <= status < 600:
print()
print(" Note: a 5xx error points to a problem on THE SERVER.")
print(" It's not your fault — the server failed to process the request.")
print()
def explore_url(url, method="GET", body=None):
"""
Explores a URL: makes the request and shows the full anatomy.
Returns a dict with the summary, or None if it failed.
"""
print()
print("*" * 65)
print(f" Exploring: {url}")
print(f" Method: {method}")
print("*" * 65)
response, error = make_request(url, method, body)
if error:
print(f"\n ERROR: {error}\n")
return None
display_request_anatomy(response)
display_response_anatomy(response)
display_diagnostics(response)
content_type = response.headers.get("Content-Type", "unknown")
content_label = "JSON" if "json" in content_type else content_type.split(";")[0]
return {
"url": url,
"status": f"{response.status_code} {response.reason}",
"time": response.elapsed.total_seconds() * 1000,
"content_type": content_label,
}
def display_summary(results):
"""Shows a comparison table when several URLs are analyzed."""
if len(results) <= 1:
return
print("=" * 65)
print(" SUMMARY — Response comparison")
print("=" * 65)
print(f" {'URL':<35} {'Status':<10} {'Time':>8} {'Type':<15}")
print(f" {'-' * 35} {'-' * 10} {'-' * 8} {'-' * 15}")
for result in results:
url_short = result["url"]
if len(url_short) > 33:
url_short = url_short[:30] + "..."
print(
f" {url_short:<35} "
f"{result['status']:<10} "
f"{result['time']:>6.0f}ms "
f"{result['content_type']:<15}"
)
print()
def main():
"""The HTTP Explorer's entry point."""
args = parse_arguments()
print()
print("+" * 65)
print(" HTTP EXPLORER v1.0")
print(" Analyzes the anatomy of HTTP transactions")
print("+" * 65)
results = []
for url in args.urls:
result = explore_url(url, args.method, args.body)
if result:
results.append(result)
display_summary(results)
print("+" * 65)
print(f" Exploration complete: {len(results)}/{len(args.urls)} URLs succeeded")
print("+" * 65)
print()
if __name__ == "__main__":
main()
The code is organized into small functions with clear responsibilities:
| Function | Responsibility |
|---|---|
parse_arguments() | Parses the CLI args with argparse |
make_request() | Runs the request and catches errors |
display_request_anatomy() | Shows what was sent |
display_response_anatomy() | Shows what came back |
get_status_category() | Categorizes the status code |
get_content_label() | Identifies the content's format |
display_diagnostics() | Shows the analysis of the result |
explore_url() | Orchestrates the analysis of one URL |
display_summary() | The comparison table for multiple URLs |
main() | The entry point |
Example runs
Example 1: A simple GET against the GitHub API
python http_explorer.py https://api.github.com/users/octocat
Expected output:
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
HTTP EXPLORER v1.0
Analyzes the anatomy of HTTP transactions
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
*****************************************************************
Exploring: https://api.github.com/users/octocat
Method: GET
*****************************************************************
=================================================================
REQUEST ANATOMY
=================================================================
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 ANATOMY
=================================================================
Status: 200 OK
Size: 1327 bytes
-----------------------------------------------------------------
Headers received:
Content-Type: application/json; charset=utf-8
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
...
-----------------------------------------------------------------
Body preview (first 500 chars):
{
"login": "octocat",
"id": 583231,
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",
"type": "User",
"name": "The Octocat",
"company": "@github",
"public_repos": 8,
...
}
=================================================================
DIAGNOSTICS
=================================================================
Status category: SUCCESS (2xx)
Response time: 245 ms
Content-Type: application/json; charset=utf-8
Content format: JSON (parseable)
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Exploration complete: 1/1 URLs succeeded
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Example 2: Comparing multiple URLs
python http_explorer.py https://api.github.com/users/octocat https://httpbin.org/get https://httpbin.org/status/404
After showing the anatomy of each URL, it prints the summary:
=================================================================
SUMMARY — Response comparison
=================================================================
URL Status Time Type
----------------------------------- ---------- -------- ---------------
https://api.github.com/users/oct... 200 OK 245ms JSON
https://httpbin.org/get 200 OK 312ms JSON
https://httpbin.org/status/404 404 Not F... 198ms HTML
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Exploration complete: 3/3 URLs succeeded
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Example 3: A POST with a body
python http_explorer.py https://httpbin.org/post --method POST --body '{"name": "HTTP Explorer", "version": 1}'
Expected output (the relevant section):
=================================================================
REQUEST ANATOMY
=================================================================
Method: POST
URL: https://httpbin.org/post
-----------------------------------------------------------------
Headers sent:
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Content-Type: application/json
Content-Length: 39
-----------------------------------------------------------------
Body sent:
{
"name": "HTTP Explorer",
"version": 1
}
Notice: requests added Content-Type: application/json and Content-Length automatically, because we used json= instead of data=.
Example 4: Error handling
python http_explorer.py https://this-does-not-exist-xyz.com
*****************************************************************
Exploring: https://this-does-not-exist-xyz.com
Method: GET
*****************************************************************
ERROR: Connection error: couldn't connect to https://this-does-not-exist-xyz.com
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Exploration complete: 0/1 URLs succeeded
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
No traceback, no crash. A clear message about what went wrong.
Project-specific troubleshooting
1. ModuleNotFoundError: No module named 'requests'
Symptom: When you run the script, Python says it can't find requests.
Cause: The library isn't installed, or you're using a different virtual environment than the one that installed it.
Fix:
pip install requests
# If you have several Python versions:
python -m pip install requests
Check that you're in the right virtual environment with which python (Mac/Linux) or where python (Windows).
2. The POST body isn't being sent
Symptom: You do a POST with --body but httpbin.org shows it didn't receive any data in the json field.
Cause: The body isn't valid JSON. Wrong quotes, a missing brace, or the shell is interpreting the characters.
Fix:
# Use single quotes around the JSON
python http_explorer.py https://httpbin.org/post --method POST --body '{"key": "value"}'
# On Windows, use escaped double quotes:
python http_explorer.py https://httpbin.org/post --method POST --body "{\"key\": \"value\"}"
3. ConnectionError on URLs that work in the browser
Symptom: A URL that opens fine in the browser gives you "Connection error" from the script.
Likely cause: The URL requires HTTPS and there's a certificate problem, or the server blocks requests that don't come from a browser (check the User-Agent).
Fix:
# Try it with curl first to rule out network problems
curl -v https://the-url-that-fails.com
# If it works with curl but not with the script, the server may be
# filtering by User-Agent. Try adding a custom header.
5. Timeouts on APIs that are normally fast
Symptom: An API that should answer quickly times out on you.
Cause: The script uses a 10-second timeout. If your network is slow or the server is overloaded, that may not be enough.
Fix: Change the timeout value in make_request() — bump it from 10 to 30.
6. The headers look different from curl's
Symptom: The request headers the script shows don't match the ones you see in curl.
Cause: requests and curl send different headers by default. requests sends User-Agent: python-requests/2.31.0, curl sends User-Agent: curl/8.x.x. Both add Accept and Accept-Encoding, but with different values.
Fix: This is normal behavior. Each tool sends its own default headers. What matters is that HTTP Explorer shows you exactly what requests sent, which is what counts for your Python code.
7. The JSON preview shows up on a single line
Symptom: The body preview isn't formatted with indentation — it all appears on one line.
Cause: The response's Content-Type doesn't include "json", so the code doesn't try to format it.
Fix: Check the Content-Type the server returns. Some APIs return text/plain even though the content is JSON. The script only formats when the Content-Type contains "json".
Completion checklist
Before you call the project done, check every point:
[ ] The script runs without errors with: python http_explorer.py https://api.github.com/users/octocat
[ ] It shows REQUEST ANATOMY: method, URL, headers sent
[ ] It shows RESPONSE ANATOMY: status code, reason, headers received, body preview
[ ] It shows DIAGNOSTICS: status category, response time, Content-Type
[ ] It works with multiple URLs and shows a SUMMARY at the end
[ ] It supports --method POST with --body '{"key": "value"}'
[ ] It handles an invalid URL without crashing (e.g. "not-a-url")
[ ] It handles a connection error without crashing (e.g. "https://does-not-exist-xyz.com")
[ ] It handles a timeout without crashing
[ ] 4xx and 5xx status codes are shown as normal responses (with a diagnosis)
[ ] The JSON body preview is shown formatted with indentation
Connection to the next module
In Module 2 you'll learn the REST principles: what a resource is, how endpoints are designed, what makes an API RESTful, and concepts like idempotency and statelessness.
What you built here — the ability to inspect any HTTP transaction — will pay off directly. When Module 2 says "a GET /users/{id} endpoint should return the resource," you'll be able to use your HTTP Explorer to verify it in practice. When it says "POST should return 201 Created," you'll be able to confirm it with your script.
HTTP Explorer is the diagnostic tool. REST principles are the design rules. In Module 2 you understand the rules; with HTTP Explorer you verify they're being followed.
And in Module 4, everything converges: you'll build the REST Client CLI, which consumes 5+ public APIs. That project uses everything from this module (HTTP anatomy, methods, status codes, headers, requests) plus what you'll learn in modules 2 and 3 (REST principles, JSON in depth, advanced tools). HTTP Explorer is the first step toward that final project.
Resources for the project
- Requests Library — Quickstart — Reference for the library you use in the script
- Requests Library — Response Objects — Documentation for the Response object and its attributes (.status_code, .headers, .text, .elapsed)
- argparse — Python Docs — The official docs for the module that parses CLI arguments
- httpbin.org — A service for testing HTTP requests (ideal for trying POST, PUT, DELETE, status codes, headers)
- MDN Web Docs — HTTP Status Codes — A complete reference of status codes and what they mean
- GitHub REST API — A public API with no authentication, good for basic testing
Next module: Module 2 — REST Principles. You'll learn the design principles that separate just-any API from a RESTful one: resources, endpoints, verbs, idempotency, and statelessness.