HTTP Field Notes

Goal

Capture three real HTTP exchanges — a public site, a redirect, and your own server — then show what file:// lacks that all of them have. Report what your phone does with http://localhost:8000. Do not fix it. ---

Type: interactive-form Unit: 3 — A second address

Goal

Capture three real HTTP exchanges — a public site, a redirect, and your own server — then show what file:// lacks that all of them have. Report what your phone does with http://localhost:8000. Do not fix it.


How this works

Run the commands, paste what comes back, do not tidy it. Fill starter/field-notes.md, then copy each block into starter/submission.json.

CommandYou get
curl -v <url>the whole conversation: > sent, < received
curl -I <url>status line and headers, no body

Your tasks

  1. TODO A. curl -v https://example.com. Paste the complete output.
  2. TODO B. curl -I a URL that answers with a 3xx. Paste the head and the URL you used.
  3. TODO C. Start python3 -m http.server 8000 in ~/projects/first-site if it is not already running. curl -I http://localhost:8000/index.html. Paste the complete head.
  4. TODO D. The first line of TODO C, on its own.
  5. TODO E. The Content-Type value from TODO C. Value only.
  6. TODO F. Annotate at least six lines from A, B, or C, in this shape:
- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`
  SOURCE: curl_i_local
  WHAT IT IS: a response header
  WHAT IT DOES: names the software that answered
  1. TODO G. What file:// has no room for, and what http://localhost:8000/index.html has instead. At least 30 words.
  2. TODO H. Type http://localhost:8000 on your phone. Write what happened, and what the server’s terminal did. At least 20 words. Report it. Do not fix it.

What the scaffolding is for

The local capture has to look like your server — HTTP/1.0 and a SimpleHTTP server header, not a generic HTTP/1.1 200 OK from a tutorial. TODO H is the wall Unit 4 exists to climb.

Expected output

TODO D looks like HTTP/1.0 200 OK. TODO E looks like text/html. TODO A still has its > and < markers.

Rules

See rubric.md for how this is scored.

Steps

Your files

Download these onto your machine and fill them in. The brief says which, and what “done” looks like.

field-notes.md Download

                    # HTTP Field Notes — worksheet

Fill in every `TODO`. Then copy each block into the matching field of `submission.json`.

---

## READ ONLY — do not edit

### The artifact this course has been building

```
~/projects/first-site/index.html
```

You made that file with commands in Unit 1 and have opened it by double-clicking ever since, which
fills the address bar with:

```
file:///home/you/projects/first-site/index.html
```

Three slots in that URL are empty and cannot be filled: a host, a port, and a program listening.
This project is where that file gets a real URL instead of a location.

### The worked annotation — this is the standard

This is one entry, in the exact format the parser expects. Read what the WHAT IT DOES half is doing.

```
- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`
  SOURCE: curl_i_local
  WHAT IT IS: a response header, sent by the server, part of the head
  WHAT IT DOES: names the software that answered me. It is how I can tell this response came from
    my own machine and not from a site on the internet — and it quietly hands anyone who asks the
    Python version I am running, which I did not choose to publish and cannot switch off.
```

Four rules the parser enforces on every entry:

1. `LINE:` is wrapped in backticks and appears **verbatim** somewhere in one of your three captures.
2. `SOURCE:` is one of `curl_v_public`, `curl_i_redirect`, `curl_i_local`.
3. `WHAT IT IS:` is not empty.
4. `WHAT IT DOES:` is at least 8 words.

The parser cannot tell whether your WHAT IT DOES half is any good. The rubric can. Do not restate
the line.

---

## TODO A — `curl_v_public`

Run `curl -v https://example.com` and paste **all** of the output below, `>` and `<` markers intact.

```
TODO A: paste here
```

## TODO B — `curl_i_redirect`

Find a URL that answers with a status code between `300` and `399`. Screen candidates fast:

```
curl -s -o /dev/null -w '%{http_code}\n' https://example.com
```

Then run `curl -I` against the one you found and paste the whole response head.

```
TODO B: paste here
```

**Which URL did you use?**

```
TODO B (url): write it here
```

## TODO C — `curl_i_local`

```
cd ~/projects/first-site
python3 -m http.server 8000
```

Leave it running. In a second terminal:

```
curl -I http://localhost:8000/index.html
```

Paste the whole response head.

```
TODO C: paste here
```

## TODO D — `status_line_local`

The first line of TODO C, on its own. Its grammar is `<protocol> <status-code> <reason-phrase>`.

```
TODO D: paste here
```

## TODO E — `content_type_local`

The **value** of the `Content-Type` header from TODO C. Value only — no header name, no colon, no
trailing spaces. Character for character.

```
TODO E: paste here
```

## TODO F — `annotations`

At least six entries, in the format of the READ ONLY worked example above. Every `LINE:` must appear
verbatim in TODO A, TODO B, or TODO C. Spread them across all three captures.

```
- LINE: `TODO F1`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:

- LINE: `TODO F2`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:

- LINE: `TODO F3`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:

- LINE: `TODO F4`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:

- LINE: `TODO F5`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:

- LINE: `TODO F6`
  SOURCE:
  WHAT IT IS:
  WHAT IT DOES:
```

Lines worth considering, if you are stuck on where to look:

- the first `>` line of TODO A — the request line, three slots
- the `>` line beginning `Host:` — the one header HTTP/1.1 makes compulsory
- the first `<` line of TODO A — the status line, with the code in the middle slot
- the status line of TODO B — a first digit that is not `2`
- any line of TODO C you did not expect to be there

## TODO G — `file_url_comparison`

Double-click `index.html` so the address bar reads
`file:///home/you/projects/first-site/index.html`. Open DevTools, go to the **Network** panel,
reload, and look at the Status column and the response headers.

Then compare against your TODO C capture. Name **both** things `file://` does not have, name at
least one header from your own capture as the contrast, and say *why* — what would have had to
happen for a status code to exist at all.

```
TODO G: write here (at least 30 words)
```

## TODO H — `phone_result`

Do this last, after everything above is filled in and your server is still running.

Pick up your phone. Same Wi-Fi as the laptop. Type into its browser:

```
http://localhost:8000
```

Write down what actually happened: what the phone's screen showed, what the laptop showed at the
same URL, and whether a new line appeared in the terminal where your server is running.

Report it. Do not fix it.

```
TODO H: write here (at least 20 words)
```

                  

submission.json Download

                    {
  "_format": "HTTP Field Notes submission. Every value is a string. Paste raw capture text; do not clean it up. Delete nothing from this file except the TODO placeholders.",

  "curl_v_public": "TODO A — the complete output of: curl -v https://example.com   (keep every > and < line)",

  "curl_i_redirect": "TODO B — the complete response head of: curl -I <a URL that answers 300-399>",

  "curl_i_redirect_url": "TODO B — the URL you used",

  "curl_i_local": "TODO C — the complete response head of: curl -I http://localhost:8000/index.html",

  "status_line_local": "TODO D — the first line of curl_i_local, on its own",

  "content_type_local": "TODO E — the value of the Content-Type header from curl_i_local. Value only.",

  "annotations": "TODO F — at least six entries in this format, one blank line between entries:\n\n- LINE: `Server: SimpleHTTP/0.6 Python/3.14.6`\n  SOURCE: curl_i_local\n  WHAT IT IS: a response header, part of the head\n  WHAT IT DOES: names the software that answered me, which is how I can tell this response came from my own machine\n",

  "file_url_comparison": "TODO G — what the file:// load has no room for, what http://localhost:8000/index.html has instead, and why. At least 30 words.",

  "phone_result": "TODO H — what happened when you typed http://localhost:8000 into your phone's browser on the same Wi-Fi, and what your server's terminal did. At least 20 words. Report it; do not fix it."
}

                  

On your machine, run the tests in this project’s tests/ folder. Each one prints PASS or a FAIL: line that names what is wrong.

How it is graded
CriterionWhat earns itWeight
Three genuine, complete captures

curl_v_public is raw curl -v output containing both > lines and < lines, with the request line and the Host: header both present among the > lines and a status line among the < lines. curl_i_redirect is a curl -I response head whose status line carries a code between 300 and 399. curl_i_local is a curl -I response head from the learner's own python3 -m http.server: its status line is HTTP/1.0 200 OK and it carries a Server: SimpleHTTP/0.6 Python/… header. status_line_local is HTTP/1.0 200 OK and matches the first line of curl_i_local. content_type_local is text/html with nothing after it.

Deduct for: a capture that has been tidied, truncated mid-header, or reflowed; a curl_v_public with the >/< markers stripped; a curl_i_local whose header set does not match its own status line (for example HTTP/1.1 next to Server: SimpleHTTP/, which is not a response any single server produced); a content_type_local that does not appear in curl_i_local.

30
Annotations explain what each line does

At least six distinct lines, each verbatim from one of the three captures, each with a WHAT IT IS half naming the line's structural job (request line, request header, status line, response header) and a WHAT IT DOES half stating what changes because that line is present.

Full credit requires that the WHAT IT DOES half is not a restatement of the line. Compare:

Restatement, no credit: "Host: example.com is the host header and it says the host is example.com."

Full credit: "Host: example.com tells the machine at the other end which of the sites it hosts this request is for. Without it an HTTP/1.1 request is answered 400, because a bare GET / is unanswerable when one address holds many sites."

Full credit also requires spread: the six lines are not all response headers from one capture. A set covering a request line, a request header, a status line, and headers from more than one response scores above a set of six headers lifted from curl_i_local alone.

Deduct for: any annotated line that cannot be found in the submitted captures; two annotations that say the same thing about two lines; a WHAT IT DOES half that only re-labels the line ("this is the date", "this is the length").

25
The file:// comparison names both absences

The comparison states that loading file:///home/you/projects/first-site/index.html produces no status line and no response headers, and that loading the identical file at http://localhost:8000/index.html produces both — quoting or naming at least one header from the learner's own curl_i_local capture as the contrast.

Full credit requires the reason, not only the observation: nothing answered, so there is nothing to have a status. A status code is a server's verdict on a request; with no server there was no request, no verdict, and no headers describing a response that never existed. The browser read the disk itself.

Deduct for: describing the difference as "fewer headers" or "the headers are hidden" rather than as an absence of the whole category; claiming the file:// load returned 200; attributing the difference to DevTools rather than to what was on the wire.

25
The phone result is reported as an observation

The learner actually tried http://localhost:8000 on a phone on the same Wi-Fi and wrote down what happened: that the page did not load, while the laptop beside it loaded the same URL, and — the detail that earns the top of this band — whether a new line appeared in the server's terminal.

Full credit requires the report to be honest and unresolved: what was typed, what was seen, and what the server's log did or did not show. Stating what localhost resolved to on the phone is credited. A submission that reports a fix, or that reports the page loading on the phone, does not receive this criterion — the first has skipped ahead of the material, and the second did not happen.

Deduct for: an empty or one-word entry; a guess written without trying it; a report that repeats the file:// comparison instead of describing the phone.

20
Total100
Test cases and grader source
Test Checks Expected Weight
local_response_authentic Catches a shortcut: Verifies that curl_i_local is a response the learner's own python3 -m http.server actually produced, and not a generic response head pasted from a tutorial. The shortcut this catches: a learner who never got their server running (or never bothered) pastes the HTTP/1.1 200 OK block that every HTTP tutorial on the internet opens with. That block is wrong in two ways that cannot be faked by accident: * This server hard-codes protocol_version = "HTTP/1.0", so its status line is HTTP/1.0 200 OK. A pasted tutorial response says HTTP/1.1. * This server always identifies itself with Server: SimpleHTTP/0.6 Python/<version>. A tutorial response carries nginx, Apache, cloudflare, or no Server header at all. PASS 30
content_type_real_file Catches a shortcut: Verifies that content_type_local is exactly text/html, with nothing after it, and that curl_i_local contains no charset anywhere. The shortcut this catches is the subtle one, and it catches a whole missing project. python3 -m http.server sends Content-type: text/html — no charset — when it reads a real .html file off the disk. It sends Content-type: text/html; charset=utf-8 only for pages it generates itself: the directory listing, and the 404 page. So a submission whose Content-Type carries charset=utf-8 did not capture a served file. It captured a directory listing. And the server only generates a directory listing when it looked inside the folder for an index page — "the directory is checked for an index page as specified by index_pages", defaulting to ("index.html", "index.htm") — and did not find one. Which means the learner never created ~/projects/first-site/index.html: the file this entire course has been building, the file the server exists to serve. They started a server in an empty or wrong directory, pointed curl at /, got a page the server wrote on the spot, and pasted it. One header, seven characters long, proves the artifact is missing. PASS 25
public_capture_both_halves Verifies that curl_v_public is real curl -v output holding BOTH halves of one conversation: at least one line curl sent (marked >) and at least one line curl received (marked <), with a request line among the sent lines and a status line among the received lines. curl -v is the only instrument in this unit that shows both messages at once. A capture with only < lines is curl -I output pasted into the wrong field; a capture with the markers stripped is a capture that was edited. PASS 15
status_line_local Verifies that status_line_local is exactly HTTP/1.0 200 OK and that it is the first non-empty line of curl_i_local — that the learner copied the status line out of their own capture rather than writing down what they expected it to say. PASS 10
redirect_is_3xx Verifies that curl_i_redirect is a response head whose status line carries a code in the Redirection class, 300-399 — a third exchange whose first digit is neither 2 nor 4. The point of the field is that the learner had to go and find one. Screening candidates with curl -s -o /dev/null -w '%{http_code}\\n' <url> is the intended method; the result must be a real capture, so this test also rejects a status line that is present without any headers under it. PASS 10
annotations_cover_six Verifies that annotations holds at least six entries, that each one quotes a DISTINCT line, that every quoted line appears verbatim somewhere in the three submitted captures, and that each entry has a non-empty WHAT IT IS and a WHAT IT DOES of at least 8 words. This test cannot judge whether an annotation is any good — that is the rubric's craft criterion. It can prove the annotated lines were really captured, which is what stops six invented headers from scoring. PASS 10

tests/test_local_response_authentic.py

                        #!/usr/bin/env python3
"""
test_local_response_authentic  —  weight 30  —  ADVERSARIAL

Verifies that `curl_i_local` is a response the learner's own `python3 -m http.server`
actually produced, and not a generic response head pasted from a tutorial.

The shortcut this catches: a learner who never got their server running (or never
bothered) pastes the `HTTP/1.1 200 OK` block that every HTTP tutorial on the internet
opens with. That block is wrong in two ways that cannot be faked by accident:

  * This server hard-codes `protocol_version = "HTTP/1.0"`, so its status line is
    `HTTP/1.0 200 OK`. A pasted tutorial response says `HTTP/1.1`.
  * This server always identifies itself with `Server: SimpleHTTP/0.6 Python/<version>`.
    A tutorial response carries nginx, Apache, cloudflare, or no Server header at all.

Prints PASS or a line beginning FAIL:.
"""

import json
import re
import sys


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def main(path):
    data = load(path)
    raw = data.get("curl_i_local", "")

    if not isinstance(raw, str) or not raw.strip():
        print("FAIL: curl_i_local is empty. Run `curl -I http://localhost:8000/index.html` "
              "against your own running server and paste the whole response head.")
        return 1

    if "TODO" in raw:
        print("FAIL: curl_i_local still contains the TODO placeholder from the starter.")
        return 1

    lines = [line.strip() for line in raw.splitlines() if line.strip()]

    status_ok = any(line.startswith("HTTP/1.0 200") for line in lines)
    server_line = next((line for line in lines
                        if re.match(r"^Server:\s*SimpleHTTP/", line, re.IGNORECASE)), None)

    if not status_ok:
        wrong_version = next((line for line in lines if line.startswith("HTTP/")), None)
        if wrong_version and wrong_version.startswith("HTTP/1.1"):
            print("FAIL: curl_i_local's status line is '%s'. `python3 -m http.server` answers "
                  "HTTP/1.0 — it hard-codes protocol_version = \"HTTP/1.0\". This response did not "
                  "come from your server." % wrong_version)
        elif wrong_version:
            print("FAIL: curl_i_local's status line is '%s'. Expected a line beginning "
                  "'HTTP/1.0 200'." % wrong_version)
        else:
            print("FAIL: curl_i_local contains no status line at all. The first line of a "
                  "`curl -I` response is <protocol> <status-code> <reason-phrase>.")
        return 1

    if server_line is None:
        present = next((line for line in lines
                        if line.lower().startswith("server:")), None)
        if present:
            print("FAIL: curl_i_local's Server header is '%s'. Your own server always sends "
                  "'Server: SimpleHTTP/0.6 Python/<version>'. This response came from some other "
                  "machine." % present)
        else:
            print("FAIL: curl_i_local has no 'Server: SimpleHTTP/...' header. "
                  "`python3 -m http.server` sends one on every response.")
        return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

tests/test_content_type_real_file.py

                        #!/usr/bin/env python3
"""
test_content_type_real_file  —  weight 25  —  ADVERSARIAL

Verifies that `content_type_local` is exactly `text/html`, with nothing after it, and
that `curl_i_local` contains no charset anywhere.

The shortcut this catches is the subtle one, and it catches a whole missing project.

`python3 -m http.server` sends `Content-type: text/html` — no charset — when it reads a
real `.html` file off the disk. It sends `Content-type: text/html; charset=utf-8` only
for pages it *generates* itself: the directory listing, and the 404 page.

So a submission whose Content-Type carries `charset=utf-8` did not capture a served
file. It captured a directory listing. And the server only generates a directory
listing when it looked inside the folder for an index page — "the directory is checked
for an index page as specified by `index_pages`", defaulting to `("index.html",
"index.htm")` — and did not find one.

Which means the learner never created `~/projects/first-site/index.html`: the file this
entire course has been building, the file the server exists to serve. They started a
server in an empty or wrong directory, pointed curl at `/`, got a page the server wrote
on the spot, and pasted it. One header, seven characters long, proves the artifact is
missing.

Prints PASS or a line beginning FAIL:.
"""

import json
import sys


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def main(path):
    data = load(path)
    value = data.get("content_type_local", "")
    capture = data.get("curl_i_local", "")

    if not isinstance(value, str) or not value.strip():
        print("FAIL: content_type_local is empty. Copy the value of the Content-Type header "
              "out of your curl_i_local capture.")
        return 1

    if "TODO" in value:
        print("FAIL: content_type_local still contains the TODO placeholder from the starter.")
        return 1

    normalised = value.strip().lower()

    if normalised.startswith("content-type:"):
        print("FAIL: content_type_local is '%s'. Copy the header's VALUE only — no header name, "
              "no colon." % value.strip())
        return 1

    if "charset" in normalised:
        print("FAIL: content_type_local is '%s'. A real .html file read off your disk gets "
              "'text/html' with no charset. The charset appears only on pages this server "
              "GENERATES — the directory listing and the 404 page. You captured a directory "
              "listing, which means the server looked in the folder for index.html and did not "
              "find one. Create ~/projects/first-site/index.html, then capture "
              "`curl -I http://localhost:8000/index.html` again." % value.strip())
        return 1

    if normalised != "text/html":
        print("FAIL: content_type_local is '%s'. Expected exactly 'text/html'." % value.strip())
        return 1

    if isinstance(capture, str) and "charset" in capture.lower():
        print("FAIL: content_type_local says 'text/html' but curl_i_local contains a charset. "
              "Those two cannot both have come from the same response. Recapture, and copy the "
              "value out of the capture rather than typing it.")
        return 1

    if isinstance(capture, str) and "text/html" not in capture.lower():
        print("FAIL: content_type_local says 'text/html' but no Content-Type header carrying "
              "'text/html' appears in curl_i_local. Copy the value out of your own capture.")
        return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

tests/test_public_capture_both_halves.py

                        #!/usr/bin/env python3
"""
test_public_capture_both_halves  —  weight 15

Verifies that `curl_v_public` is real `curl -v` output holding BOTH halves of one
conversation: at least one line curl sent (marked `>`) and at least one line curl
received (marked `<`), with a request line among the sent lines and a status line
among the received lines.

`curl -v` is the only instrument in this unit that shows both messages at once. A
capture with only `<` lines is `curl -I` output pasted into the wrong field; a capture
with the markers stripped is a capture that was edited.

Prints PASS or a line beginning FAIL:.
"""

import json
import re
import sys

METHODS = ("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def main(path):
    data = load(path)
    raw = data.get("curl_v_public", "")

    if not isinstance(raw, str) or not raw.strip():
        print("FAIL: curl_v_public is empty. Run `curl -v https://example.com` and paste the "
              "complete output.")
        return 1

    if "TODO" in raw:
        print("FAIL: curl_v_public still contains the TODO placeholder from the starter.")
        return 1

    sent, received = [], []
    for line in raw.splitlines():
        stripped = line.strip()
        if stripped.startswith(">"):
            sent.append(stripped[1:].strip())
        elif stripped.startswith("<"):
            received.append(stripped[1:].strip())

    if not sent and not received:
        print("FAIL: curl_v_public contains no '>' lines and no '<' lines. Either -v was not "
              "used, or the markers were removed when the output was cleaned up. Paste the "
              "output exactly as curl printed it.")
        return 1

    if not sent:
        print("FAIL: curl_v_public contains %d received ('<') lines but no sent ('>') lines. "
              "The request half of the conversation is missing." % len(received))
        return 1

    if not received:
        print("FAIL: curl_v_public contains %d sent ('>') lines but no received ('<') lines. "
              "The response half of the conversation is missing." % len(sent))
        return 1

    if not any(line.split(" ")[0] in METHODS and line.endswith(("HTTP/1.0", "HTTP/1.1", "HTTP/2",
                                                                "HTTP/3"))
               for line in sent if line):
        print("FAIL: none of the '>' lines is a request line. A request line is three "
              "space-separated slots: <method> <request-target> <protocol>, for example "
              "'GET / HTTP/1.1'.")
        return 1

    if not any(re.match(r"^HTTP/[0-9.]+\s+[1-5][0-9]{2}", line) for line in received):
        print("FAIL: none of the '<' lines is a status line. A status line is "
              "<protocol> <status-code> <reason-phrase>, and the code is a three-digit number "
              "in the middle slot.")
        return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

tests/test_status_line_local.py

                        #!/usr/bin/env python3
"""
test_status_line_local  —  weight 10

Verifies that `status_line_local` is exactly `HTTP/1.0 200 OK` and that it is the first
non-empty line of `curl_i_local` — that the learner copied the status line out of their
own capture rather than writing down what they expected it to say.

Prints PASS or a line beginning FAIL:.
"""

import json
import sys

EXPECTED = "HTTP/1.0 200 OK"


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def main(path):
    data = load(path)
    value = data.get("status_line_local", "")
    capture = data.get("curl_i_local", "")

    if not isinstance(value, str) or not value.strip():
        print("FAIL: status_line_local is empty. Copy the first line of your curl_i_local "
              "capture.")
        return 1

    if "TODO" in value:
        print("FAIL: status_line_local still contains the TODO placeholder from the starter.")
        return 1

    normalised = " ".join(value.split())

    if normalised != EXPECTED:
        print("FAIL: status_line_local is '%s'. Expected '%s' — the three slots of a status line "
              "are <protocol> <status-code> <reason-phrase>, and this server answers HTTP/1.0."
              % (normalised, EXPECTED))
        return 1

    first = next((line.strip() for line in str(capture).splitlines() if line.strip()), "")
    if " ".join(first.split()) != EXPECTED:
        print("FAIL: status_line_local says '%s' but the first non-empty line of curl_i_local is "
              "'%s'. Copy the status line out of your own capture." % (EXPECTED, first))
        return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

tests/test_redirect_is_3xx.py

                        #!/usr/bin/env python3
"""
test_redirect_is_3xx  —  weight 10

Verifies that `curl_i_redirect` is a response head whose status line carries a code in
the Redirection class, 300-399 — a third exchange whose first digit is neither 2 nor 4.

The point of the field is that the learner had to go and find one. Screening candidates
with `curl -s -o /dev/null -w '%{http_code}\\n' <url>` is the intended method; the
result must be a real capture, so this test also rejects a status line that is present
without any headers under it.

Prints PASS or a line beginning FAIL:.
"""

import json
import re
import sys

CLASSES = {
    1: "Informational (100-199) — not the final answer yet",
    2: "Successful (200-299) — you asked, and you got it",
    4: "Client error (400-499) — your request was wrong",
    5: "Server error (500-599) — the server broke",
}


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def main(path):
    data = load(path)
    raw = data.get("curl_i_redirect", "")

    if not isinstance(raw, str) or not raw.strip():
        print("FAIL: curl_i_redirect is empty. Find a URL that answers with a code between 300 "
              "and 399 and paste the `curl -I` response head.")
        return 1

    if "TODO" in raw:
        print("FAIL: curl_i_redirect still contains the TODO placeholder from the starter.")
        return 1

    lines = [line.strip() for line in raw.splitlines() if line.strip()]
    status = next((line for line in lines if re.match(r"^HTTP/[0-9.]+\s", line)), None)

    if status is None:
        print("FAIL: curl_i_redirect has no status line. The first line of a `curl -I` response "
              "begins with the protocol, for example 'HTTP/1.1 301 Moved Permanently'.")
        return 1

    match = re.match(r"^HTTP/[0-9.]+\s+([0-9]{3})", status)
    if match is None:
        print("FAIL: curl_i_redirect's status line is '%s' and carries no three-digit status "
              "code in the middle slot." % status)
        return 1

    code = int(match.group(1))
    if not 300 <= code <= 399:
        family = CLASSES.get(code // 100, "an unrecognised class")
        print("FAIL: curl_i_redirect's status code is %d, which is %s. This field wants a "
              "Redirection code, 300-399. Screen candidate URLs with "
              "`curl -s -o /dev/null -w '%%{http_code}\\n' <url>` until one prints a number "
              "beginning with 3." % (code, family))
        return 1

    header_lines = [line for line in lines
                    if line is not status and re.match(r"^[A-Za-z][A-Za-z0-9-]*:\s*\S", line)]
    if not header_lines:
        print("FAIL: curl_i_redirect has a %d status line but no response headers under it. "
              "Paste the whole response head, not just the first line." % code)
        return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

tests/test_annotations_cover_six.py

                        #!/usr/bin/env python3
"""
test_annotations_cover_six  —  weight 10

Verifies that `annotations` holds at least six entries, that each one quotes a DISTINCT
line, that every quoted line appears verbatim somewhere in the three submitted captures,
and that each entry has a non-empty WHAT IT IS and a WHAT IT DOES of at least 8 words.

This test cannot judge whether an annotation is any good — that is the rubric's craft
criterion. It can prove the annotated lines were really captured, which is what stops
six invented headers from scoring.

Prints PASS or a line beginning FAIL:.
"""

import json
import re
import sys

MINIMUM = 6
MIN_DOES_WORDS = 8
CAPTURE_FIELDS = ("curl_v_public", "curl_i_redirect", "curl_i_local")


def load(path):
    with open(path, "r", encoding="utf-8") as handle:
        return json.load(handle)


def parse(raw):
    """Split the annotations blob into entries keyed by LINE."""
    entries = []
    current = None
    for line in raw.splitlines():
        stripped = line.strip()
        match = re.match(r"^-?\s*LINE:\s*`(.+?)`\s*$", stripped)
        if match:
            current = {"line": match.group(1), "is": "", "does": ""}
            entries.append(current)
            continue
        if current is None:
            continue
        is_match = re.match(r"^WHAT IT IS:\s*(.*)$", stripped, re.IGNORECASE)
        if is_match:
            current["is"] = is_match.group(1).strip()
            current["_last"] = "is"
            continue
        does_match = re.match(r"^WHAT IT DOES:\s*(.*)$", stripped, re.IGNORECASE)
        if does_match:
            current["does"] = does_match.group(1).strip()
            current["_last"] = "does"
            continue
        if re.match(r"^SOURCE:", stripped, re.IGNORECASE):
            current["_last"] = None
            continue
        if stripped and current.get("_last"):
            current[current["_last"]] = (current[current["_last"]] + " " + stripped).strip()
    return entries


def main(path):
    data = load(path)
    raw = data.get("annotations", "")

    if not isinstance(raw, str) or not raw.strip():
        print("FAIL: annotations is empty. Annotate at least %d lines from your captures."
              % MINIMUM)
        return 1

    if "TODO" in raw:
        print("FAIL: annotations still contains TODO placeholders from the starter.")
        return 1

    entries = parse(raw)

    if len(entries) < MINIMUM:
        print("FAIL: found %d annotation entries, need at least %d. Each entry starts with a "
              "line of the form:  - LINE: `<the captured line>`" % (len(entries), MINIMUM))
        return 1

    seen = set()
    for entry in entries:
        if entry["line"] in seen:
            print("FAIL: the line `%s` is annotated more than once. The %d entries must quote %d "
                  "DISTINCT lines." % (entry["line"], MINIMUM, MINIMUM))
            return 1
        seen.add(entry["line"])

    haystack = "\n".join(str(data.get(field, "")) for field in CAPTURE_FIELDS)

    for entry in entries:
        if entry["line"] not in haystack:
            print("FAIL: the annotated line `%s` does not appear in curl_v_public, "
                  "curl_i_redirect, or curl_i_local. Annotate lines you actually captured, and "
                  "copy them character for character." % entry["line"])
            return 1
        if not entry["is"]:
            print("FAIL: the entry for `%s` has an empty WHAT IT IS. Name the line's job: "
                  "request line, request header, status line, response header."
                  % entry["line"])
            return 1
        words = len(entry["does"].split())
        if words < MIN_DOES_WORDS:
            print("FAIL: the entry for `%s` has a WHAT IT DOES of %d word(s); at least %d are "
                  "required. Say what changes because that line is there, not what the line says."
                  % (entry["line"], words, MIN_DOES_WORDS))
            return 1

    print("PASS")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "submission.json"))

                      

Erase saved progress?

This erases all quiz scores, reading progress, project checklists, and your name on the certificate. It cannot be undone, and it affects only this course in this browser.