Course / 06 Web basics / Lesson 06 of 08

curl, and one HTTP request from Python

Estimated time: 15 min reading · 15 min activity

The earlier lessons supplied HTTP messages. curl is a command-line client that can request a URL and display the response. Here it reads one note from a server on your own machine. You can follow the full recorded transcript in your browser or later reproduce the steps in your lab.

The option -i includes response headers in the displayed output alongside the body. The option -o takes a filename and writes the transferred output there. Without -i, this HTTP example saves the body alone. Options can be combined, so inspect the actual command before assuming a saved file contains only content.

127.0.0.1 is the loopback address for this host; it does not identify an external server. The URL uses port 8000 and path /note.txt. The file URL file:///home/desk/lab-site/note.txt instead reads a local file directly. It does not exercise HTTP or a listening server.

Before you begin

Estimated time
15 min reading, 15 min activity. Setup and optional practice may take longer.
Where you work
The browser route uses the complete recorded transcript. For a later lab attempt, use an owned Ubuntu/Python environment, Python 3.12 and an available curl command. Create a fresh directory under ~/yanosec-lab containing only these lesson files. The server binds to 127.0.0.1:8000 and serves only a lab-site child containing note.txt. Use two terminals and stop the server with Ctrl+C. Installation time is separate.
Without a lab
The cards provide the names, byte counts, requests, responses, and stored records needed for the activity. Make a prediction and compare with the supplied reasoning; a practical run can wait. Reading works without JavaScript. Answer checking needs JavaScript and a secure context, normally HTTPS or a local preview. Saving additionally needs usable browser storage.

After this lesson you can:

  • Separate the response status and headers from the file body.
  • Explain the loopback server’s address, directory, startup, and stop procedure.
  • Choose the curl option that includes response headers.

01Example

The command shows how -i requests response headers in curl’s output. The stable response lines below come from the earlier 2026-10-12 local run using Python 3.12.3 and curl 8.5.0. The historical header/body listing was assembled from separately captured headers and body; it is not one verbatim curl -i stdout record. Date, Server, and Last-Modified lines are omitted here.

curl -i http://127.0.0.1:8000/note.txt

Observed stable lines
HTTP/1.0 200 OK
Content-type: text/plain
Content-Length: 11

owned note

02One client, one file

The supplied note contains owned note followed by LF, the one-byte newline 0a. The space counts too: its complete size is 11 bytes. The server sends those bytes as the body and adds HTTP status and headers around them. curl transfers that response; it does not render the body as a browser page.

The local server command is python3 -m http.server 8000 --bind 127.0.0.1 --directory lab-site. The -m option asks Python to run the named module. The next number selects the listening port. --bind selects the address, limiting this example to local connections; --directory selects the folder whose contents are served.

Create a fresh parent directory and place the downloaded fetch-note.py there. Create its lab-site child and save the supplied note as lab-site/note.txt. Keep the server directory limited to that note, with no symbolic links, which are filesystem references that could lead outside the folder. Python’s simple server can follow such links. It is a local teaching server, not a production service.

Python 3.12’s default server response version here is HTTP/1.0 even though a client can send an HTTP/1.1 request. A response version describes that message. In the recorded run, Content-type was text/plain, Content-Length was 11, and the body was the supplied note. Time-related headers depend on the run and file.

The Python client imports urlopen from urllib.request, a standard-library module for opening URLs. with closes the response after use; response.status is the numeric HTTP status. response.read() obtains body bytes, decode("utf-8") interprets them as text, and print(body, end="") avoids adding a second newline. The final if line checks whether this file was launched directly; if it was, it calls the main function.

Inspect the inherited 2026-10-12 stable record: status and headers precede an 11-byte owned note body with final LF.Saved trace. Output recorded once in a course lab and saved here. You read it; you do not need to reproduce it.

Read the full explanation below.

Command whose -i option includes headers

curl -i http://127.0.0.1:8000/note.txt
Include response headers

Inherited stable header/body excerpt, assembled from separate captures

HTTP/1.0 200 OK
Content-type: text/plain
Content-Length: 11
 
owned note
 
Response status lineBody media typeRecorded body byte countBody text with recorded final LF
What this part means

-i requests response headers in the displayed output as well as the body.

This is not a newly run request.

-i requests response headers in the displayed output as well as the body.

Follow the explanation

  1. -i requests response headers in the displayed output as well as the body.
  2. Status 200, Content-type text/plain and Content-Length 11 are response metadata.
  3. The body is owned note plus LF: ten visible characters including the space, then one newline byte, total 11 bytes.
Read every state and value

1. -i requests response headers in the displayed output as well as the body.

Selected parts
Include response headers
Explanation
include

2. Status 200, Content-type text/plain and Content-Length 11 are response metadata.

Selected parts
Response status line · Body media type · Recorded body byte count
Explanation
headers

3. The body is owned note plus LF: ten visible characters including the space, then one newline byte, total 11 bytes.

Selected parts
body · Recorded body byte count
Explanation
body
Inspect the inherited 2026-10-12 stable record: status and headers precede an 11-byte owned note body with final LF.

03Headers in the output, body in a file

For the browser route, follow the commands as actions in the supplied record and predict each result before reading its recorded lines. For a later lab run, use the fresh folder described above. Keep the server in one terminal and the clients in a second terminal, both starting in that parent folder.

"""Read one loopback note. The printed result is 200, then owned note. An earlier desk-note run is a different file."""

from urllib.request import urlopen


def main():
    with urlopen("http://127.0.0.1:8000/note.txt") as response:
        print(response.status)
        body = response.read().decode("utf-8")
        print(body, end="")


if __name__ == "__main__":
    main()

Download the Python client

Recorded local exchange for lab-site/note.txt, 2026-10-12.
Python 3.12.3 and curl 8.5.0. The header/body listing combines
separately captured response headers and body; it is not one verbatim
curl -i stdout record.
An earlier fetch of desk-note on port 8765
is a different file.

Command whose option includes headers in output
curl -i http://127.0.0.1:8000/note.txt

The handler's default protocol was HTTP/1.0. The response also sent
Server: SimpleHTTP/0.6 Python/3.12.3, a Date, and Last-Modified.
Last-Modified followed the file's modification time. Those three headers
are not needed for the interpretation below.

HTTP/1.0 200 OK
Content-type: text/plain
Content-Length: 11

owned note

Content-Length 11 is the byte length of lab-site/note.txt, including its final LF.
Content-type for this file was text/plain.
A request for /missing.txt returned HTTP/1.0 404 File not found.

Activity command
curl -o note-copy.txt http://127.0.0.1:8000/note.txt

Observed contents of note-copy.txt
owned note

Check: note-copy.txt
That file contains the body. The separate -i command requests headers as well.

python3 fetch-note.py against the same server
200
owned note

A file URL such as file:///home/desk/lab-site/note.txt reads the local file.
It is a different operation from the HTTP URL above.

Download the observed transcript

  1. In the first terminal, start python3 -m http.server 8000 --bind 127.0.0.1 --directory lab-site. It remains running to handle requests, so leave that terminal open. The browser route identifies these options in the transcript instead.
  2. In the second terminal, enter curl -i http://127.0.0.1:8000/note.txt. Expect a status line, response headers, a blank line, then owned note. Compare the stable recorded lines: status 200, text/plain, length 11.
  3. Use a fresh destination: enter curl -o note-copy.txt http://127.0.0.1:8000/note.txt only where note-copy.txt does not already exist. Open that copy in your editor. It should contain the body owned note and its final LF, without HTTP headers.
  4. From the same parent folder, enter python3 fetch-note.py. Its recorded result is 200 followed by owned note. On the browser route, trace read and decode to explain those lines instead of claiming a new run.
  5. In the first terminal, press Ctrl+C to stop the server. The shell prompt should return. Stop when you can explain the two curl outputs and the Python result; retain only your own notes or copies and leave no server running.

04Optional challenge

For about fifteen extra minutes in a later local lab attempt, request /missing.txt from the same server. The earlier record received HTTP/1.0 404 File not found, meaning the request reached a server that reported an absent file. Compare that with a refused connection, which supplies no HTTP response. Stop your server afterward.

05Fresh cases for the checks

Use these fictional cases for the checks below. They have different inputs from the worked examples. Read them here or make notes; no installation or external service is needed.

HTTP client case A: fictional local setupOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

Server command: python3 -m http.server 8164 --bind 127.0.0.1 --directory leaf-site
Only supplied invented files belong in leaf-site; no symbolic links or private files.
Request goal: include response headers while retrieving http://127.0.0.1:8164/entry.txt with curl.
Stop the foreground local server afterward using the taught interrupt.

HTTP client case B: independent authored responseOwned fixture. An original file made for this course. Follow the activity's instructions in your own lab.

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 6
 
ready
The body is ready followed by LF; it has six bytes. This is an authored message, not a captured curl run.

06Explain your prediction

Optional written self-check: Record which lines are status, headers and body, plus the server directory and loopback boundary. A refused connection supplies no HTTP response, while an HTTP 404 comes from a responding server.

07Common mistakes

If note-copy.txt includes headers, inspect whether -i was also used. The supplied -o command has no -i, so its saved transfer is the body. Do not overwrite the original note to fix a copy.

If port 8000 is already occupied, stop this attempt and identify your own existing server before changing anything. Do not stop someone else’s process. A different owned port must be used consistently in the server, every URL, and a disposable Python-client copy.

If a connection is refused, check that your first terminal still has the server running with the stated address and port. If a 404 arrives instead, check lab-site/note.txt and the requested path. These are different failure stages.

If a server was started without the loopback bind, stop it with Ctrl+C and restart with the full command. The default can listen on all interfaces.

References