curl -I and the browser DevToolsHTTP (HyperText Transfer Protocol) is the language a browser and a web server use to talk. HTTPS is the same language, locked with encryption. HTTP lives in the Application layer.
Every web visit has two parts:
Simple meaning:
Browser ---- request: "GET /page" ----> Server
Browser <--- response: 200 OK + headers + page --- Server
A response has three parts:
HTTP/1.1 200 OK <-- status line: version, code, short text
Content-Type: text/html <-- headers: notes about the response
Cache-Control: max-age=3600
<-- blank line
<h1>Hello</h1> <-- body: the actual page or data
A status code is a 3-digit number that tells you how the request went. A header is one line of information in the form Name: value. Headers travel with both requests and responses.
Think of ordering food at a restaurant.
You (browser) = The customer
Server = The restaurant
Request = Your order
Status code = The waiter's first words
Headers = Notes on the order slip and on the tray
Body = The food (the web page)
The waiter's first words tell you what happened:
The first digit tells you the class.
| Class | Meaning | Restaurant version |
|---|---|---|
| 1xx | Informational | "Hold on, working on it" (rarely seen) |
| 2xx | Success | "Here you go!" |
| 3xx | Redirection | "Go to another counter" |
| 4xx | Client error | "Your order has a problem" |
| 5xx | Server error | "Our kitchen has a problem" |
| Code | Name | Meaning |
|---|---|---|
| 200 | OK | It worked. Here is what you asked for |
| 201 | Created | A new thing was made, such as a new user after sign-up |
| 301 | Moved Permanently | The page has a new address for good. Follow the Location header |
| 302 | Found | The page is somewhere else for now. Come back to the old address later |
| 400 | Bad Request | The server could not understand your request, such as broken data |
| 401 | Unauthorized | You are not logged in, or your login is missing or wrong |
| 403 | Forbidden | The server knows who you are, but you are not allowed |
| 404 | Not Found | Nothing exists at this address |
| 500 | Internal Server Error | The server hit a bug or crashed |
| 502 | Bad Gateway | A server in the middle got a bad reply from the server behind it |
Easy ways to tell similar codes apart:
| Header | Sent in | What it does | Example |
|---|---|---|---|
| Content-Type | Request and response | Says what kind of data the body is | text/html, application/json |
| Authorization | Request | Proves who you are | Bearer demo-token-123 |
| Cookie | Request | Sends back small notes the server gave you earlier | session=abc123 |
| Cache-Control | Response | Says how long the answer can be saved | max-age=3600 (1 hour), no-store (do not save) |
| User-Agent | Request | Says which browser or tool is asking | curl/8.5.0 |
A few more you will see below:
โ ๏ธ Authorization and Cookie values are secrets. Never share real ones in screenshots or messages.
There are no real accounts to risk, so we will make a tiny test server on your own computer. It listens only on 127.0.0.1, which means only your own computer can reach it.
Create a file named test_server.py with this content. You do not need to understand it. It just returns different status codes for different addresses.
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def reply(self, code, body="", content_type="text/html; charset=utf-8", extra=None):
data = body.encode()
self.send_response(code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
for name, value in (extra or {}).items():
self.send_header(name, value)
self.end_headers()
if self.command != "HEAD":
self.wfile.write(data)
def handle_any(self):
path = self.path
if path == "/":
self.reply(200, "<h1>Hello from the test server</h1>",
extra={"Cache-Control": "max-age=3600"})
elif path == "/old":
self.reply(301, extra={"Location": "/new"})
elif path == "/temp":
self.reply(302, extra={"Location": "/new"})
elif path == "/new":
self.reply(200, "<h1>You reached the new page</h1>")
elif path == "/private":
if self.headers.get("Authorization") == "Bearer demo-token-123":
self.reply(200, "<h1>Welcome, you are logged in</h1>")
else:
self.reply(401, "<h1>Please log in</h1>",
extra={"WWW-Authenticate": "Bearer"})
elif path == "/admin":
self.reply(403, "<h1>You are not allowed here</h1>")
elif path == "/boom":
self.reply(500, "<h1>Something broke on the server</h1>")
elif path == "/gateway":
self.reply(502, "<h1>Bad reply from the server behind me</h1>")
elif path == "/login":
self.reply(200, "<h1>Logged in</h1>",
extra={"Set-Cookie": "session=abc123; HttpOnly"})
elif path == "/whoami":
info = {"user_agent": self.headers.get("User-Agent"),
"cookie": self.headers.get("Cookie")}
self.reply(200, json.dumps(info), "application/json")
elif path == "/users" and self.command == "POST":
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length)
try:
user = json.loads(raw)
except ValueError:
self.reply(400, json.dumps({"error": "Body is not valid JSON"}),
"application/json")
return
user["id"] = 1
self.reply(201, json.dumps(user), "application/json",
extra={"Location": "/users/1"})
else:
self.reply(404, "<h1>Page not found</h1>")
do_GET = do_HEAD = do_POST = handle_any
ThreadingHTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
Linux:
python3 test_server.py
Windows:
python test_server.py
Nothing prints. That is normal. Leave this window open and open a second terminal for the next steps. To stop the server later, press Ctrl + C in its window.
curl is a tool that makes web requests from the terminal. The -I option asks for headers only.
On Windows PowerShell, use curl.exe instead of curl. PowerShell has its own command named curl.
curl -I http://127.0.0.1:8000/
Output (real, from my run):
HTTP/1.1 200 OK
Server: BaseHTTP/0.6 Python/3.13.17
Date: Thu, 29 Oct 2026 18:44:34 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 35
Cache-Control: max-age=3600
200 OK is the status lineContent-Type says the body is a web pageCache-Control: max-age=3600 says the answer can be saved for 1 hourcurl -I http://127.0.0.1:8000/old
Output (real, from my run):
HTTP/1.1 301 Moved Permanently
Server: BaseHTTP/0.6 Python/3.13.17
Date: Thu, 29 Oct 2026 18:44:34 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Location: /new
The Location header says where to go. Add -L to make curl follow it:
curl -IL http://127.0.0.1:8000/old
Output (real, from my run, shortened):
HTTP/1.1 301 Moved Permanently
Location: /new
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Two answers: first the 301, then the 200 from the new address. Try /temp too. It gives a 302.
curl -I http://127.0.0.1:8000/missing
Output (real, from my run, shortened):
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
Content-Length: 23
Without a login:
curl -I http://127.0.0.1:8000/private
Output (real, from my run, shortened):
HTTP/1.1 401 Unauthorized
Content-Type: text/html; charset=utf-8
WWW-Authenticate: Bearer
Now send the Authorization header. This token is made up for the test:
curl -I -H "Authorization: Bearer demo-token-123" http://127.0.0.1:8000/private
Output (real, from my run, shortened):
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Same address. With the right header, 401 became 200.
A POST request sends data to the server, for example to create a user. -i shows the headers and the body.
Linux:
curl -i -X POST -H "Content-Type: application/json" -d '{"name": "Asha"}' http://127.0.0.1:8000/users
Windows (Command Prompt):
curl -i -X POST -H "Content-Type: application/json" -d "{\"name\": \"Asha\"}" http://127.0.0.1:8000/users
Output (real, from my run, shortened):
HTTP/1.1 201 Created
Content-Type: application/json
Location: /users/1
{"name": "Asha", "id": 1}
Here Content-Type tells the server the data is JSON. Now send broken data:
curl -i -X POST -H "Content-Type: application/json" -d "not json" http://127.0.0.1:8000/users
Output (real, from my run, shortened):
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error": "Body is not valid JSON"}
-v (verbose) shows the headers you send (lines with >) and the headers you get back (lines with <).
curl -v -H "Cookie: session=abc123" http://127.0.0.1:8000/whoami
Output (real, from my run, trimmed):
> GET /whoami HTTP/1.1
> Host: 127.0.0.1:8000
> User-Agent: curl/8.5.0
> Accept: */*
> Cookie: session=abc123
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 56
<
{"user_agent": "curl/8.5.0", "cookie": "session=abc123"}
curl sent its own User-Agent: curl/8.5.0. The server read both headers and repeated them back.
for p in / /old /temp /missing /private /admin /boom /gateway; do echo -n "$p -> "; curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000$p; done
Output (real, from my run):
/ -> 200
/old -> 301
/temp -> 302
/missing -> 404
/private -> 401
/admin -> 403
/boom -> 500
/gateway -> 502
DevTools are tools built into your browser (Chrome, Edge, Firefox) that show what happens behind a page.
http://127.0.0.1:8000/old in your browserF12 to open DevTools (on a Mac: Cmd + Option + I)F5You will see two rows: old with status 301, then new with status 200.
old row, then open the Headers tabYou will see:
Location: /newUser-Agent and AcceptNow try cookies. Open http://127.0.0.1:8000/login (the server gives you a cookie with Set-Cookie). Then open http://127.0.0.1:8000/whoami. It shows your browser's User-Agent text and "cookie": "session=abc123". Your browser sent that cookie by itself.
| Error / Symptom | Cause | Fix |
|---|---|---|
curl: (7) Failed to connect to 127.0.0.1 port 8000 |
The test server is not running | Start it in another window with python3 test_server.py |
OSError: [Errno 98] Address already in use (Linux) |
An old copy of the server is still running | Stop it with Ctrl + C, or change 8000 in the script and in your URLs |
| Still 401 with the token | A typo in the header | It must be exactly Authorization: Bearer demo-token-123 |
| 404 on a page that should exist | A typo in the address | Check the spelling. Many servers care about capital letters |
405 Method Not Allowed with curl -I on a real site |
That site does not allow header-only requests | Use curl -i instead, which does a normal request |
curl -I fails in PowerShell |
In PowerShell, curl is a nickname for a different command |
Use curl.exe -I |
| DevTools Network tab is empty | You opened it after the page had loaded | Keep it open and reload with F5 |
| Redirect rows disappear in DevTools | The log was cleared on the new page | Tick Preserve log |
| 502 or 500 on a real website | The problem is on their server | Wait and try again. Site owners should check their server |
When something fails, read the first digit of the status code first. 4xx means check your own request (the address, your login, your data). 5xx means it is not your fault. Then look at the headers for clues.
- HTTP = request + response. A response has a status line, headers and a body
- 1xx info, 2xx success, 3xx redirect, 4xx your mistake, 5xx server mistake
- 200 OK, 201 Created, 301 moved for good, 302 moved for now
- 400 bad request, 401 not logged in, 403 not allowed, 404 not found
- 500 server broke, 502 bad reply from the server behind
- Content-Type = kind of data, Authorization = proof of login
- Cookie = notes sent back, Cache-Control = how long to save, User-Agent = who is asking
- curl -I = headers only, curl -v = headers you send (>) and get (<)
- DevTools: F12 > Network > click a request > Headers
- Never share real Authorization or Cookie values
TechWithJuned ยท Learn โ Execute โ Build
๐ฌ Questions & Discussion
Have a doubt? Ask below โ no login needed.
Loading comments...