Embercode
✕
How the Internet WorksLesson 4 of 15

55 min read

HTTP requests and responses

Why this matters

Once a client has an address, it still has to say something. The language of the web is HTTP, and every web app, every API and every mobile app that talks to a server speaks it. When your code "calls an API", it is sending an HTTP request and reading an HTTP response. Every error number you will ever debug in web development (404, 401, 500) comes from this lesson. If you can read a raw request and response, you can find out in minutes what a framework is doing behind your back.

The idea: a form with fixed boxes

HTTP stands for HyperText Transfer Protocol. A protocol, as you know, is an agreed set of rules. HTTP's rules are simple and very regular, like a paper form with fixed boxes:

  1. The client sends one request message.
  2. The server sends back one response message.
  3. That is the whole exchange. Then the server forgets about you.

Think of ordering by letter from a catalogue. You write a letter in a fixed layout: first line says what you want, then lines with extra details, then a blank line, then (if needed) an attachment. The shop replies in a similar layout: first line says how it went, then detail lines, a blank line, then the goods. Because both sides follow the layout exactly, a shop in one country can serve a customer using a completely different kind of computer.

Two properties define HTTP:

  • Request-response. The client always asks first. The server only replies.
  • Stateless. Each request stands alone. The server does not automatically remember who you were a moment ago. (Cookies, later in this lesson, are the workaround.)

HTTP is text-based in its classic form (version 1.1). That is a gift for learners: you can read the actual messages with your own eyes. Newer versions (HTTP/2 and HTTP/3) pack the same messages into a faster binary format, but the ideas, the methods, the status codes and the headers are the same. Your code and your browser's developer tools show them in the familiar form.

HTTP rides on top of TCP, which you met in the first lesson. First the client opens a TCP connection to the server's port (80 for http, 443 for https), then it writes the request text onto the connection, and the server writes the response back.

An HTTP request and its response side by sideAn HTTP request and its response side by side

Anatomy of a request

A request has four parts, always in this order:

  1. The request line: method, target, version.
  2. Headers: one Name: value per line.
  3. A blank line. It marks the end of the headers.
  4. An optional body: data sent along (for example the contents of a form).

Lines end with two invisible characters, carriage return and line feed, written \r\n. That is the formal line ending of HTTP.

GET /products?id=7 HTTP/1.1
Host: shop.example.com
User-Agent: demo-client/1.0
Accept: text/html
 

The parts on the first line:

  • Method: GET. The verb. What the client wants to do.
  • Target: /products?id=7. The path and query from the URL (the scheme, host and fragment are not here).
  • Version: HTTP/1.1.

Here is a request written as a Python string, and a few lines of code that take it apart. This is what every web server does with the bytes it receives, just more carefully.

Python · editable

Output:

method : GET
target : /products?id=7
version: HTTP/1.1
headers: {'host': 'shop.example.com', 'user-agent': 'demo-client/1.0', 'accept': 'text/html'}
body   : ''

Walk through the code. raw.partition("\r\n\r\n") cuts the message at the first blank line into a head and a body. Here the body is empty, because a GET request normally has none. The first line of the head is split on spaces into three parts. Each later line is split on the first colon only (split(":", 1)), because a header value may itself contain colons, like a time or a URL. The names are lower-cased so that later lookups are not case-sensitive.

Why is there a Host header when the connection already went to the right address? Because one server address can host many sites. The Host header says which site this request is for. Without it a server with three websites would not know which to answer. HTTP/1.1 makes it required.

Anatomy of a response

The response has the same shape: a status line, headers, a blank line and an optional body.

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 27
 
<h1>Hello</h1><p>World</p>!

The status line holds the version, a three-digit status code (200) and a short human reason phrase (OK). The code is what programs read. The phrase is only a hint for people.

Python · editable

Output:

version: HTTP/1.1 | code: 200 | reason: OK
{'Content-Type': 'text/html; charset=utf-8', 'Content-Length': '27'}
declared length: 27
actual length  : 27

Two details. The status line is split with maxsplit=2 (split(" ", 2)), because the reason phrase can contain spaces ("Not Found", "Internal Server Error") and must stay in one piece. And Content-Length counts bytes, not characters. For plain English text they are equal. With accented letters or emoji they are not, as you will see below.

Methods: what the client wants to do

The method is the verb of the request. The common ones:

MethodMeaningHas a body?Example
GETRead a resourceNoLoad a page, fetch a list of users
POSTSend data to be processed, usually creating somethingYesSubmit a sign-up form
PUTReplace a resource with the one you sendYesOverwrite a profile
PATCHChange part of a resourceYesUpdate just the email address
DELETERemove a resourceUsually noDelete a comment
HEADLike GET, but return only headersNoCheck whether a file exists and how big it is
OPTIONSAsk what is allowedNoUsed by browsers before some cross-site calls

Two ideas help you pick the right one.

A method is safe if it should not change anything on the server. GET and HEAD are safe. This is why a search engine or a link preview can request any URL without breaking anything, and why deleting something through a GET link is a serious design error: a crawler following links would wipe your data.

A method is idempotent if doing it twice has the same effect as doing it once. PUT, DELETE, GET and HEAD are idempotent. Deleting user 5 twice leaves the same world as deleting it once (user 5 is gone). POST is not: posting an order twice creates two orders. This matters for retries. If a network hiccup makes the client unsure whether a request arrived, it can safely repeat a PUT, but repeating a POST risks a duplicate (a reason payment pages say "do not press back or refresh").

Status codes: how it went

The first digit of the status code gives the class. Learn the classes first, the individual numbers second.

ClassMeaningCommon codes
1xxInformational100 Continue
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirect: look somewhere else301 Moved Permanently, 302 Found, 304 Not Modified
4xxClient error: the request is wrong400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 405 Method Not Allowed, 429 Too Many Requests
5xxServer error: the server failed500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout
Python · editable

Output:

200 -> success
201 -> success
204 -> success
301 -> redirect: look somewhere else
304 -> redirect: look somewhere else
400 -> client error: your request is wrong
401 -> client error: your request is wrong
403 -> client error: your request is wrong
404 -> client error: your request is wrong
429 -> client error: your request is wrong
500 -> server error: the server failed
503 -> server error: the server failed
700 -> not an HTTP status

Many people in web work check only the class: "it's a 4xx, so I sent something wrong" or "it's a 5xx, so it is not my fault, the server broke". That split decides where to look first.

The codes you will meet daily, in plain language:

  • 200 OK: it worked, and the body is the answer.
  • 201 Created: it worked and something new now exists (the usual answer to a successful POST that creates).
  • 204 No Content: it worked, and there is nothing to send back (typical for DELETE).
  • 301 Moved Permanently and 302 Found: the resource lives at another URL, given in the Location header. 301 says "update your bookmarks, it moved for good"; 302 says "use this other URL for now".
  • 304 Not Modified: you already have the latest copy in your cache, use it.
  • 400 Bad Request: the server cannot understand your request (broken JSON, a missing field).
  • 401 Unauthorized: you have not proven who you are. (The name is misleading. It really means "unauthenticated".)
  • 403 Forbidden: the server knows who you are and says you may not do this.
  • 404 Not Found: nothing at this path. Also used on purpose to hide that something exists.
  • 405 Method Not Allowed: the path exists but not for this method, for example POST to a page that only supports GET.
  • 429 Too Many Requests: you are being rate-limited. Slow down.
  • 500 Internal Server Error: the server's code crashed.
  • 502 Bad Gateway and 504 Gateway Timeout: a server in the middle (such as a load balancer) got a bad or no answer from the server behind it.
  • 503 Service Unavailable: the server is overloaded or down for maintenance.

A toy web server

To see how a server decides, here is a small router. It takes the method, path and optional body, and returns a status code and a text. Real frameworks do exactly this, only larger. Read the code before you run it and try to predict each line of output.

Python · editable

Output:

GET    /          -> 200 home page
POST   /          -> 405 Method Not Allowed
GET    /users     -> 200 Asha, Ben
POST   /users     -> 201 created user 3
POST   /users     -> 400 name is required
GET    /users/3   -> 200 Chen
DELETE /users/1   -> 204 
GET    /users/1   -> 404 no such user
GET    /nothing   -> 404 Not Found

Trace the state changes, which is the important part. The first GET /users lists Asha and Ben. POST /users with the body Chen adds a third user with id 3 (the largest existing id plus one) and answers 201. The next POST has an empty body, so the server refuses with 400 and changes nothing. GET /users/3 now finds Chen. DELETE /users/1 removes Asha and answers 204 with an empty body. The later GET /users/1 is 404, because the delete took effect. Notice how every outcome is chosen by the server's logic, and each status code has a clear reason.

The 405 branch is a good design detail: when the path exists but the method does not fit, the right answer is 405, not 404. A 404 would wrongly tell the client the path itself is missing.

Headers

Headers are extra information about the request or response, in Name: value form. Header names are case-insensitive: Content-Type, content-type and CONTENT-TYPE mean the same. HTTP/2 even requires lower case. Here is a tiny class that gets this right, next to a plain dictionary that gets it wrong.

Python · editable

Output:

text/html
text/html
(not sent)
None

The plain dictionary returns None because it treats the two spellings as different keys. Many real bugs come from that, so libraries wrap headers in a case-insensitive structure.

The headers you need to know first:

In requests:

  • Host: which site (required in HTTP/1.1).
  • User-Agent: what kind of client this is (browser name and version).
  • Accept: what formats the client can handle, such as text/html or application/json.
  • Content-Type: the format of the body being sent, such as application/json.
  • Content-Length: the body size in bytes.
  • Authorization: credentials, such as Bearer abc123.
  • Cookie: the cookies the browser is sending back.

In responses:

  • Content-Type: the format of the body, such as text/html; charset=utf-8 or application/json. The browser decides how to treat the body based on this. HTML gets rendered, an image gets drawn, and an unknown type gets downloaded.
  • Content-Length: body size in bytes.
  • Location: where to go next, used with 3xx redirects (and with 201).
  • Set-Cookie: please store this cookie.
  • Cache-Control: how long this may be reused without asking again (the next lesson covers caching).

You will also see Server, Date, ETag, Access-Control-Allow-Origin and many more, but the list above covers most of what you read.

Media types

The value of Content-Type is a media type (also called MIME type): a type/subtype label. text/html, text/css, application/json, image/png, application/pdf. For text it can carry a character set (charset=utf-8), which tells the receiver how to turn bytes into letters. A wrong Content-Type is a classic reason for a browser to show raw code instead of a page, or for a server to refuse your data.

The body and Content-Length

For methods that send data, the body carries it. The most common format for APIs is JSON, text that looks like {"name":"Zoë"}. The sender sets Content-Type: application/json so the receiver knows to parse it, and Content-Length so the receiver knows where the body ends.

Here the length is computed in JavaScript. Look at the difference between characters and bytes.

JavaScript · editable

Output:

{"name":"Zoë","tags":["a","b"]}
characters: 31
bytes     : 32
POST /users HTTP/1.1\r\n
Host: api.example.com\r\n
Content-Type: application/json\r\n
Content-Length: 32\r\n
\r\n
{"name":"Zoë","tags":["a","b"]}

The letter ë is one character but two bytes in UTF-8, so the body has 31 characters and 32 bytes. The header must say 32. If it said 31, the server would read one byte too few and cut the last } off, then fail to parse the JSON. In practice your library computes this for you. The point is to know where the number comes from, so a "Content-Length mismatch" error makes sense. The last print uses replaceAll to make the invisible line endings visible, which is a good trick for debugging.

Redirects

A redirect is a response that says "not here, go there". The status is 3xx and a Location header gives the new URL. A client then makes a second request. Browsers do this silently, and you only see the address bar change.

Python · editable

Output:

GET /old -> 301
GET /new -> 302
GET /final -> 200
You arrived

The limit guards against a redirect loop. If /a redirects to /b and /b back to /a, an unguarded client would circle forever. Browsers stop after about 20 hops and show ERR_TOO_MANY_REDIRECTS. Typical uses: forcing http to https, moving a page to a new address, and sending you to a login page.

Four related codes are worth separating. 301 and 308 are permanent and 302 and 307 are temporary. The older 301 and 302 let a browser change a POST into a GET on the second request, while 307 and 308 require the same method to be kept.

Cookies: memory for a stateless protocol

Since HTTP forgets you between requests, how does a shop remember your cart, or a site remember you are logged in? With a cookie: a small piece of text the server asks the browser to store and send back with every later request to that site.

  1. You log in. The server responds with Set-Cookie: session=abc123; HttpOnly; Path=/; Max-Age=3600.
  2. The browser saves it.
  3. On every later request to that site, the browser adds Cookie: session=abc123.
  4. The server looks up abc123 in its records and knows it is you.
Python · editable

Output:

Set-Cookie: session=abc123; HttpOnly; Max-Age=3600; Path=/
session = abc123
theme = dark

The attributes after the value are instructions to the browser. HttpOnly hides the cookie from JavaScript on the page, which makes it harder to steal. Max-Age=3600 says to keep it for 3600 seconds. Path=/ says it applies to the whole site. You will also meet Secure (send only over HTTPS) and SameSite (limits sending on cross-site requests). Note that the cookie travels in a header, so it is visible in the developer tools, and treating its value as a secret means it must only travel over HTTPS.

The sensible design is to store only a random session id in the cookie and keep the real data on the server. Never store a password in a cookie.

Seeing real HTTP

You do not have to take any of this on trust. Two tools show the real messages.

The browser's developer tools. Open any site, press F12 (or Cmd+Option+I on a Mac), choose the Network tab and reload. Every row is one request-response pair. Click one and open "Headers" to see the request line, request headers, status and response headers. "Response" or "Preview" shows the body. This will be your most-used debugging window for the next year.

curl, a command-line client. Run these in your terminal:

curl -i https://example.com
curl -v https://example.com
curl -X POST -H "Content-Type: application/json" -d '{"name":"Asha"}' https://api.example.com/users
curl -I https://example.com

-i prints the response headers together with the body. -v (verbose) also shows the request that curl sent, with lines starting with > for what you sent and < for what came back. -X POST sets the method, -H adds a header and -d supplies the body. -I sends a HEAD request. The start of a typical curl -i answer looks like this (the exact headers vary from server to server):

HTTP/2 200
content-type: text/html
content-length: 1256
cache-control: max-age=604800
 
<!doctype html>
...

Notice HTTP/2 and the lower-case header names. That is the newer protocol version, shown in a text form for readability. The fields, the status code and the body are exactly what you have learned.

Your own code does the same with a few lines. In JavaScript the standard call is fetch:

const response = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Asha" }),
});
console.log(response.status);
const data = await response.json();

Everything in the object is a piece of the request you have just studied: the method, the headers, the body. The status is the number from the response's first line.

What happens inside a server

When the server's TCP connection receives bytes, a loop like this runs:

  1. Read bytes until the blank line (\r\n\r\n). That is the end of the head.
  2. Parse the request line and headers.
  3. If a Content-Length header exists, read exactly that many more bytes as the body. (Another option is Transfer-Encoding: chunked, where the body arrives in pieces with sizes, used when the sender does not know the total length in advance.)
  4. Find the route that matches the method and path, and run its code.
  5. Write the status line, headers, blank line and body back.
  6. Either keep the TCP connection open for the next request (keep-alive, the default in HTTP/1.1, because opening connections is slow) or close it.

A framework like Express, Flask or Spring does steps 1 to 3 and 5 for you, and you write step 4. Knowing the steps explains odd behaviours, such as why a server hangs forever when a client promises a Content-Length of 100 and sends only 50 bytes: the server is faithfully waiting for the other 50.

HTTP/2 and HTTP/3 send many requests at once over one connection and compress the headers. This is why modern pages, which fetch dozens of files, load faster. Your code does not change.

Designing responses: a worked example

Putting methods and status codes together is the daily work of anyone who builds an API. Take a small notes service and decide what each request should answer.

RequestSituationResponse
GET /notesAnyone logged in200 with a list in the body
GET /notesNo credentials401
POST /notes with a valid JSON bodyLogged in201, a Location: /notes/17 header and the new note in the body
POST /notes with broken JSONLogged in400, with a message saying what is wrong
POST /notes with valid JSON but an empty titleLogged in422 (or 400, depending on the team's convention) with the field named
GET /notes/17Note exists, belongs to you200
GET /notes/17Note belongs to someone else403 (or 404, to avoid admitting that it exists)
GET /notes/9999No such note404
DELETE /notes/17Succeeds204, no body
PUT /notesCollection does not accept PUT405 with an Allow: GET, POST header
Any requestDatabase is down503, ideally with Retry-After
Any requestBug in the handler500

Read the table as a set of questions to ask for every endpoint: who may call it, what can be wrong with the input, what happens when the thing does not exist, and what happens when our own code fails. Each question maps to a status class. Nothing here needs more than the codes you already know, plus 422 ("I understood your request, but its content is not acceptable"), which many teams use for validation failures.

Two habits make an API pleasant to use. First, give errors a consistent body, such as {"error": "title is required", "field": "title"}, so that front-end code can show it. Second, never leak internals in a 500: log the stack trace on the server and send the client a short, harmless message.

Seen from the client side, the same table drives the code you write after a fetch: branch on the class of response.status first, then on the specific code where it matters. A 401 sends the user to the login page, a 404 shows "not found", a 429 waits and retries, and a 5xx shows a "try again" message.

Common mistakes

1. Confusing 401 and 403. 401 means "I do not know who you are" (log in). 403 means "I know who you are and the answer is no". If a logged-in user hits 403, logging in again will not help.

2. Returning 404 when 405 is right (or 200 for errors). Some beginners reply 200 with a body {"error": "not found"}. Clients and tools that look at the status code assume success. Use the status code to carry the outcome.

3. Sending JSON without Content-Type. Your server framework may then not parse the body, and your handler sees an empty or missing value. In Express, req.body is undefined if the JSON-parsing step is missing or the header is not application/json. In fetch, set the header explicitly, as in the example above.

4. Putting a body on a GET. Many servers and proxies ignore or reject it. Use query parameters for GET and a body for POST.

5. Using GET for actions that change data. Browsers prefetch links, search engines crawl them, and caches repeat them. A link /delete?id=3 will eventually be followed by something that is not a person. Use POST or DELETE.

6. Reading Content-Length as characters. It counts bytes. A mismatch gives hangs (too large) or cut-off bodies and parse errors (too small).

7. Treating a 500 as the client's fault. It means the server's own code threw an error. Look at the server's logs, not at the request, though the request tells you how to reproduce it.

8. Believing that "no error in the browser" means "it worked". fetch does not throw on 404 or 500. It only throws when the network fails. You must check response.ok or response.status yourself:

const res = await fetch("/api/users/999");
if (!res.ok) {
  throw new Error("Request failed with status " + res.status);
}

9. Forgetting that cookies and headers are visible. Anyone who can see the traffic or the developer tools sees them. Only HTTPS protects them on the wire.

10. The CORS error. In the browser console you will meet:

Access to fetch at 'http://localhost:4000/api' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

It does not mean your request was wrong. The browser sent it, the server answered, and the browser then refused to let your page read the answer, because the two addresses are different origins (from the URL lesson) and the server did not say it is okay. The fix is on the server: add the Access-Control-Allow-Origin header for your front end's origin.

A debugging walkthrough

Scenario: your sign-up form sends a POST to /api/users. The server answers 400 Bad Request. Find out why.

  1. See the exact request. In the Network tab, click the failing request. Check the method (POST?), the URL, the Content-Type request header, and the "Payload" (the body).
  2. See the exact response. Open the Response tab. Well-built servers explain the 400 in the body: {"error": "email is required"}. Read the body before anything else.
  3. Compare with what the server expects. The body shows {"Email": "a@b.com"} but the server wants email, in lower case. JSON keys are case-sensitive, so the field is missing.
  4. Reproduce with curl. curl -i -X POST -H "Content-Type: application/json" -d '{"email":"a@b.com"}' http://localhost:3000/api/users. A 201 here confirms that the server is fine and your front end sent the wrong key.
  5. Fix and re-check the Network tab.

The process works for nearly every API problem: look at the real request, look at the real response, compare with the documentation, reproduce outside your app, change one thing. For a 5xx, the same method applies, but step 2 moves to the server logs.

Variations and edge cases

  • Query versus body. Search filters go in the query of a GET. Passwords and large payloads go in the body of a POST.
  • Content negotiation. The Accept header lets one URL return HTML to a browser and JSON to an API client.
  • Conditional requests. A client can send If-None-Match with a stored ETag value. If nothing changed, the server replies 304 Not Modified with no body and saves bandwidth.
  • Range requests. Video players ask for only part of a file with Range: bytes=0-999999 and get 206 Partial Content.
  • Compression. Accept-Encoding: gzip, br lets the server shrink text before sending. The response carries Content-Encoding: gzip.
  • Long-lived connections. WebSockets start as an HTTP request, then upgrade to a two-way channel where the server can push messages. It is how live chats work.
  • Timeouts and retries. Clients give up after a set time. Retrying a safe or idempotent request is fine. Retrying a POST needs care.
  • Idempotency keys. Payment APIs let you send a unique key with a POST so a repeated request does not charge twice.

Where this shows up in real work and interviews

  • Building an API means choosing methods, paths and status codes well. The popular style, called REST, maps nouns to paths and verbs to methods: GET /users, POST /users, DELETE /users/42.
  • "What is the difference between GET and POST?" A good answer covers safety, bodies, caching, and the fact that data in a GET goes into the URL, which is logged and visible.
  • "What is the difference between PUT and PATCH?" PUT replaces a whole resource, PATCH changes part of it.
  • "What does a 401 mean versus a 403?" Unauthenticated versus unauthorised.
  • "What is the difference between a 301 and a 302?" Permanent versus temporary, which changes how browsers and search engines cache the move.
  • "Is HTTP stateful?" No, and cookies and tokens add state on top.
  • Production alerts are often phrased in status codes: "5xx rate above 1 percent". You now know what is being counted.

Practice

1. (Easy) In GET /cart HTTP/1.1, name the method, the target and the version.

Solution

Method GET, target /cart, version HTTP/1.1.

2. (Easy) Which status class means "the client made a mistake" and which means "the server failed"?

Solution

4xx means the client made a mistake (a bad request). 5xx means the server failed.

3. (Easy) Which method would you use to read a list of products? To create an order? To remove a comment?

Solution

GET to read the list, POST to create an order, DELETE to remove a comment.

4. (Easy) What does the blank line in an HTTP message mean?

Solution

It ends the headers. Anything after it is the body, which may be empty.

5. (Medium) A user is logged in but gets 403 when they open /admin. A logged-out visitor gets 401 on /account. Explain both.

Solution

The visitor on /account has not proven who they are, so the server says 401 (unauthenticated). The user on /admin is known, but their account lacks permission, so the server says 403 (forbidden). Logging in fixes the first. Only a change in permissions fixes the second.

6. (Medium) Write the full raw text of a POST request to /login on host app.example.com with the JSON body {"user":"asha"}. Compute the Content-Length.

Solution

The body {"user":"asha"} has 15 characters, all ASCII, so 15 bytes: braces 2, "user" 6, colon 1, "asha" 6, which sums to 15.

POST /login HTTP/1.1
Host: app.example.com
Content-Type: application/json
Content-Length: 15
 
{"user":"asha"}

7. (Medium) Explain why DELETE /users/5 sent twice is idempotent but POST /orders sent twice is not.

Solution

After the first DELETE, user 5 no longer exists. The second DELETE finds nothing to remove, so the state of the server is the same (the response may be 404, but nothing else changes). Two POST /orders create two separate orders, so the state differs from sending one.

8. (Medium) Your front end shows "Something went wrong", but the Network tab shows the request returned 200 with the body {"error":"user not found"}. What is wrong with the server and why is it a problem?

Solution

The server reports a failure but with a success status. Clients, monitoring tools, caches and your own code (response.ok) treat 200 as success. The server should answer 404 Not Found (or another fitting 4xx) with the error message in the body. Fix the server's status code, not just the front end.

9. (Hard) Write a Python function parse_request(raw) that returns a dictionary with method, path, query (as a dictionary of first values) and headers (lower-cased names) and test it on a request with a query string.

Solution
from urllib.parse import urlparse, parse_qs
 
def parse_request(raw):
    head, _, body = raw.partition("\r\n\r\n")
    lines = head.split("\r\n")
    method, target, _version = lines[0].split(" ")
    url = urlparse(target)
    query = {k: v[0] for k, v in parse_qs(url.query).items()}
    headers = {}
    for line in lines[1:]:
        name, value = line.split(":", 1)
        headers[name.strip().lower()] = value.strip()
    return {"method": method, "path": url.path, "query": query,
            "headers": headers, "body": body}
 
raw = "GET /search?q=shoes&page=2 HTTP/1.1\r\nHost: shop.example.com\r\n\r\n"
print(parse_request(raw))

It prints {'method': 'GET', 'path': '/search', 'query': {'q': 'shoes', 'page': '2'}, 'headers': {'host': 'shop.example.com'}, 'body': ''}. The key points: split the head from the body, split the target into path and query, take the first value of each query key, and split headers on the first colon only.

10. (Hard) A page loads, but its stylesheet shows up as plain text instead of being applied. The request for /style.css returned 200. Name a likely header problem and how you would confirm it.

Solution

The response probably has a wrong Content-Type, such as text/plain or text/html, instead of text/css. Browsers use that header to decide what to do with a body, and some refuse to apply a stylesheet with the wrong type. Confirm by opening the Network tab, clicking /style.css and reading the response headers, or by running curl -I https://yoursite.example/style.css. Fix it in the server or static-file configuration.

11. (Hard) A client sends Content-Length: 120 with a body of only 100 bytes, and the server stops responding. Explain, and say what the client sees.

Solution

The server follows the header: it reads exactly 120 bytes. After 100 bytes it is still waiting for the other 20, which never come, so it does not run the handler. The client, waiting for a response, sees a hanging request until a timeout fires, usually reported as a timeout error or 408 Request Timeout from the server. The fix is to make Content-Length match the real byte count, which libraries do automatically.

What you now know

  • HTTP is a text protocol where the client sends one request and the server sends one response. It is stateless.
  • A request is a request line (method, target, version), headers, a blank line and an optional body. A response is a status line, headers, a blank line and an optional body.
  • Methods say what to do: GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes. Safe and idempotent methods can be retried safely.
  • Status code classes: 2xx success, 3xx redirect, 4xx client error, 5xx server error. Know 200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 405, 429, 500 and 503 by heart.
  • Headers carry the details: Host, Content-Type, Content-Length, Authorization, Cookie, Set-Cookie and Location. Names are case-insensitive and Content-Length counts bytes.
  • Cookies give a stateless protocol a memory: the server sets one and the browser sends it back.
  • To debug, read the real request and response in the Network tab or with curl -i.

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← What is a URL?