Computing FoundationsHow the Internet Works › Day 21

Hands-on lab — Day 21: Inspecting Traffic with curl and Developer Tools

Commands

Setup

cd labs/sections/computing-foundations/day-021-inspecting-traffic-with-curl-and-developer

Run

bash examples/debug_request.sh
bash starter/debug_request.sh

Test

bash tests/run_tests.sh

File tree

examples/debug_request.sh
expected-output/sample-run.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/debug_request.sh
starter/debug-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 021 lab — Debug a Request with curl

Lesson

Purpose

Day 21's lesson showed how to see what happens on the wire. This lab puts the tools in your hands: you use curl to inspect a full request/response conversation, follow a redirect, break down timing, prove which headers you sent, read status codes deliberately, and walk a systematic decision tree that diagnoses any failing request.

Learning objectives

  • Read a curl -v conversation and tell the connection notes, request, and response headers apart.
  • Follow a redirect with -L and read the final status.
  • Break a request's time into DNS, connect, TLS, and total with -w.
  • Classify status codes (2xx/3xx/4xx/5xx) and know what each tells you to do.
  • Apply a DNS → connect → TLS → status decision tree to diagnose a failure.

Prerequisites

  • The Day 21 lesson, and days 15–20 (the networking category).
  • A terminal with curl and network access.

Supported operating systems

  • macOS and Linux — fully supported (curl preinstalled).
  • Windowscurl ships with Windows 10+; or run under WSL.

Hardware requirements

Any computer with an internet connection. The lab only makes ordinary web requests.

Required software

curl only — preinstalled everywhere. See requirements/README.md.

Free and open-source options

Everything here is free: curl is open source, and the two test services (example.com, httpbin.org) are free public endpoints. No accounts or API keys.

Installation

None. Change into this directory:

cd labs/sections/computing-foundations/day-021-inspecting-traffic-with-curl-and-developer

File structure

day-021-.../
├── README.md
├── metadata.yml
├── starter/
│   ├── debug_request.sh          ← YOUR working file (5 exercises)
│   └── debug-worksheet.md        ← record your captured values
├── examples/
│   └── debug_request.sh          ← completed reference implementation
├── tests/
│   └── run_tests.sh
├── expected-output/
│   └── sample-run.txt            ← real captured run
├── requirements/README.md
├── troubleshooting.md
└── security.md

How to run

## 1. See the finished tool first (needs network)
bash examples/debug_request.sh

## 2. Complete the five exercises in the starter, then run it
bash starter/debug_request.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/debug_request.sh — runs six inspections against example.com and httpbin.org: a full curl -v conversation, a followed redirect (-IL), a timing breakdown (-w), a headers echo (-H against /headers), deliberate 404/500 status reads, and a diagnose() function that walks the DNS → connect → TLS → status decision tree. It degrades gracefully offline.
  • bash starter/debug_request.sh — the same skeleton with five numbered exercises; each names the exact curl command to use.
  • bash tests/run_tests.sh — always verifies structure (files present, both scripts parse, the example exercises each core flag, the starter names five exercises), then — when online — checks the three key behaviours: curl -L follows the redirect to a 200, curl reads the deliberate 404 correctly, and the /headers endpoint echoes a header sent with -H. Offline, the live checks are skipped (never failed). Exits 0 on success.

Expected output

See expected-output/sample-run.txt for a real captured run. Your IPs, timings, and (for httpbin) transient statuses will differ.

Validation steps

  1. bash examples/debug_request.sh prints all six sections without errors.
  2. You can point at each *, >, < line in the -v output and say whether it is a connection note, your request, or the response.
  3. The redirect section shows a 3xx first, then a 200 after -L.
  4. The tests pass.

Tests

bash tests/run_tests.sh

Expected final line: 11 checks, 0 failure(s), 0 skip(s). online (some checks skip, never fail, when offline). Exit 0 on success.

Cleanup

Nothing to clean up — the scripts only make web requests and write no files. Reset your edited starter with git checkout -- starter/debug_request.sh.

Troubleshooting

See troubleshooting.md — httpbin 503s, reading -v output, redirects, timing interpretation, offline behavior.

Security notes

See security.md. Only public test endpoints; never send real secrets to an echo service; redact curl -v headers before sharing.

Extension exercises

  1. Add a diagnose() call for a deliberately broken URL (a made-up hostname) and confirm it reports the DNS gate as FAILED.
  2. Use curl -w to compare the total time of a cold request versus an immediate second request to the same host, and explain the difference.
  3. Reproduce a 401 by calling https://httpbin.org/basic-auth/user/pass without credentials, then with -u user:pass, and read both status codes.
  • Previous day: Day 20 — How Browsers Render: HTML, CSS, and JavaScript.
  • Next day: Day 22 — What an API Is and Why Everything Has One.

Expected output

sample-run.txt

Day 021 — Debug a Request with curl
Test servers: https://example.com and https://httpbin.org

=== 1. curl -v — the full request/response conversation ===
$ curl -v https://example.com   (connection notes and headers shown)
* IPv6: 2606:4700:10::ac42:93f3, 2606:4700:10::6814:179a
* IPv4: 172.66.147.243, 104.20.23.154
* Connected to example.com (2606:4700:10::ac42:93f3) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=example.com
*  start date: May 31 21:39:12 2026 GMT
*  expire date: Aug 29 21:41:26 2026 GMT
*  subjectAltName: host "example.com" matched cert's "example.com"
*  issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://example.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 200 
< date: Sun, 12 Jul 2026 08:58:20 GMT
< content-type: text/html
< server: cloudflare
< last-modified: Wed, 01 Jul 2026 17:52:37 GMT
< allow: GET, HEAD
< accept-ranges: bytes
< age: 11548
< cf-cache-status: HIT
< cf-ray: a19ed8753cb11672-BOM
< 
* Connection #0 to host example.com left intact

=== 2. curl -IL — follow a redirect (httpbin/redirect/1 -> /get) ===
$ curl -IL https://httpbin.org/redirect/1
first request:  HTTP 302  ->  location: https://httpbin.org/get
after -L follows the location header, final status: HTTP 200

=== 3. curl -w — timing breakdown (seconds) ===
$ curl -o /dev/null -s -w '...' https://example.com
dns:0.002618  connect:0.014062  tls:0.031457  ttfb:0.048168  total:0.048289

=== 4. curl -H — the headers endpoint echoes what you sent ===
$ curl -H 'Accept: application/json' https://httpbin.org/headers
{
  "headers": {
    "Accept": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/8.7.1", 
    "X-Amzn-Trace-Id": "Root=1-6a53574c-1b9215523e94e5fa08cb459b"
  }
}


=== 5. Reading status codes — a deliberate 404 and 500 ===
$ curl -o /dev/null -s -w '%{http_code}' https://httpbin.org/status/404
404 endpoint returned: HTTP 503  — a SERVER error — the server failed a valid request
$ curl -o /dev/null -s -w '%{http_code}' https://httpbin.org/status/500
500 endpoint returned: HTTP 503  — a SERVER error — the server failed a valid request
Note: httpbin's error endpoints can themselves return 502/503/504 under load — still a 5xx, same lesson.

=== 6. diagnose() — walk the decision tree for https://example.com ===
Gate 1 (DNS):     ok  — resolved in 0.003803s
Gate 2 (connect): ok  — TCP connected in 0.014318s
Gate 3 (TLS):     ok  — handshake done in 0.032559s
Gate 4 (status):  ok  — HTTP 200 (success)

=== 6. diagnose() — walk the decision tree for https://httpbin.org/status/404 ===
Gate 1 (DNS):     ok  — resolved in 0.002327s
Gate 2 (connect): ok  — TCP connected in 0.260098s
Gate 3 (TLS):     ok  — handshake done in 0.786573s
Gate 4 (status):  HTTP 503 — SERVER error: the server failed a valid request

Done. Every section above maps to a stage of the failed-request decision tree.

Source files

examples/debug_request.sh (8821 bytes)
#!/usr/bin/env bash
# Day 021 lab — Debug a Request with curl (completed reference implementation).
#
# Demonstrates the core traffic-inspection skills against two public test
# servers: https://httpbin.org (echoes requests, returns any status you ask
# for) and https://example.com. Every section maps to a stage of the
# failed-request decision tree from the lesson.
#
# Run from the lab directory:  bash examples/debug_request.sh
#
# Requires network access. If the network (or the test server) is
# unreachable, each section degrades gracefully with a clear message and the
# script still exits 0, so it is safe to run offline.
set -u

HTTPBIN="https://httpbin.org"
EXAMPLE="https://example.com"

rule() { printf '\n=== %s ===\n' "$1"; }

# Return 0 if we appear to have network access to the test host, else 1.
have_network() {
  curl -s -o /dev/null --max-time 12 "${EXAMPLE}" 2>/dev/null
}

# ---------------------------------------------------------------------------
# 1. curl -v : see the whole conversation (DNS, TCP, TLS, request, response).
# ---------------------------------------------------------------------------
show_verbose() {
  rule "1. curl -v — the full request/response conversation"
  echo "\$ curl -v ${EXAMPLE}   (connection notes and headers shown)"
  # -v prints to stderr; keep the * (connection), > (request) and < (response
  # header) lines and drop the noisy download-progress meter and body.
  curl -v -o /dev/null --max-time 15 "${EXAMPLE}" 2>&1 \
    | grep -E '^[*<>]' \
    | grep -vE 'Trying|Establish|schannel|CApath|CAfile|TLSv.*handshake' \
    || echo "(could not reach ${EXAMPLE})"
}

# ---------------------------------------------------------------------------
# 2. curl -IL : follow a redirect to its final destination, headers only.
# ---------------------------------------------------------------------------
show_redirect() {
  rule "2. curl -IL — follow a redirect (httpbin/redirect/1 -> /get)"
  echo "\$ curl -IL ${HTTPBIN}/redirect/1"
  # Shown as two explicit steps so the output is always clean regardless of
  # httpbin's transient gateway hiccups. -w writes its line ONCE after the
  # final transfer (not per retry), and -o /dev/null discards the header dump,
  # so there are never duplicate lines. A real 302 is not a transient error,
  # so --retry returns it as-is; a transient 5xx is retried away.
  local hop1 code1 loc final
  hop1="$(curl -sI -o /dev/null --max-time 25 --retry 5 --retry-delay 1 \
    -w '%{http_code} %{redirect_url}' "${HTTPBIN}/redirect/1" 2>/dev/null)" || hop1=""
  if [ -z "${hop1}" ]; then echo "(could not reach ${HTTPBIN})"; return; fi
  read -r code1 loc <<< "${hop1}"
  echo "first request:  HTTP ${code1}  ->  location: ${loc:-<none>}"
  final="$(curl -o /dev/null -sIL --max-time 25 --retry 5 --retry-delay 1 \
    -w '%{http_code}' "${HTTPBIN}/redirect/1" 2>/dev/null)" || final="000"
  echo "after -L follows the location header, final status: HTTP ${final}"
}

# Describe a status code by its class — always truthful about what came back.
classify_status() {
  case "$1" in
    2*) echo "success" ;;
    3*) echo "a redirect — follow it with -L" ;;
    4*) echo "a CLIENT error — fix your request (URL, auth, or body)" ;;
    5*) echo "a SERVER error — the server failed a valid request" ;;
    *)  echo "no valid response (timeout or connection failure)" ;;
  esac
}

# ---------------------------------------------------------------------------
# 3. curl -w : timing breakdown — which stage was slow?
# ---------------------------------------------------------------------------
show_timing() {
  rule "3. curl -w — timing breakdown (seconds)"
  echo "\$ curl -o /dev/null -s -w '...' ${EXAMPLE}"
  curl -o /dev/null -s --max-time 15 \
    -w 'dns:%{time_namelookup}  connect:%{time_connect}  tls:%{time_appconnect}  ttfb:%{time_starttransfer}  total:%{time_total}\n' \
    "${EXAMPLE}" \
    || echo "(could not reach ${EXAMPLE})"
}

# ---------------------------------------------------------------------------
# 4. curl -H : prove which headers you actually sent (echoed back).
# ---------------------------------------------------------------------------
show_headers_echo() {
  rule "4. curl -H — the headers endpoint echoes what you sent"
  echo "\$ curl -H 'Accept: application/json' ${HTTPBIN}/headers"
  # -f (--fail) makes curl print nothing and return non-zero on an HTTP error,
  # so a transient 503 page is never dumped; --retry rides through the hiccup.
  curl -sf --max-time 25 --retry 4 --retry-delay 2 -H "Accept: application/json" "${HTTPBIN}/headers" \
    || echo "(could not reach ${HTTPBIN} — transient server error; try again in a moment)"
  echo
}

# ---------------------------------------------------------------------------
# 5. Read status codes deliberately: a 4xx (client) and a 5xx (server).
# ---------------------------------------------------------------------------
show_status_codes() {
  rule "5. Reading status codes — a deliberate 404 and 500"
  echo "\$ curl -o /dev/null -s -w '%{http_code}' ${HTTPBIN}/status/404"
  local code404 code500
  # --retry 5 rides through httpbin's transient gateway 5xx (502/503/504) to
  # get the deterministic 404; a real 404 is NOT a transient error, so curl
  # returns it without retrying.
  code404="$(curl -o /dev/null -s --max-time 25 --retry 5 --retry-delay 1 -w '%{http_code}' "${HTTPBIN}/status/404")" || code404="000"
  printf '404 endpoint returned: HTTP %s  — %s\n' "${code404}" "$(classify_status "${code404}")"
  # No --retry here: curl treats 5xx as transient and would retry it away;
  # we WANT to see the server error, whatever 5xx httpbin returns this run.
  echo "\$ curl -o /dev/null -s -w '%{http_code}' ${HTTPBIN}/status/500"
  code500="$(curl -o /dev/null -s --max-time 25 -w '%{http_code}' "${HTTPBIN}/status/500")" || code500="000"
  printf '500 endpoint returned: HTTP %s  — %s\n' "${code500}" "$(classify_status "${code500}")"
  echo "Note: httpbin's error endpoints can themselves return 502/503/504 under load — still a 5xx, same lesson."
}

# ---------------------------------------------------------------------------
# 6. A systematic diagnose function: walk the decision tree for any URL.
# ---------------------------------------------------------------------------
diagnose() {
  local url="$1"
  rule "6. diagnose() — walk the decision tree for ${url}"
  # Ask curl for the status code and each timing gate in one shot.
  local out code namelookup connect appconnect
  out="$(curl -o /dev/null -s --max-time 25 \
    -w '%{http_code} %{time_namelookup} %{time_connect} %{time_appconnect}' \
    "${url}" 2>/dev/null)" || out=""

  if [ -z "${out}" ]; then
    echo "Gate 1 (DNS/connect): FAILED — host did not resolve or connect. Check the name and your network."
    return
  fi
  read -r code namelookup connect appconnect <<< "${out}"

  # Gate 1: DNS resolved if namelookup time is > 0.
  if awk "BEGIN{exit !(${namelookup} > 0)}"; then
    echo "Gate 1 (DNS):     ok  — resolved in ${namelookup}s"
  else
    echo "Gate 1 (DNS):     FAILED — hostname did not resolve"; return
  fi
  # Gate 2: TCP connected.
  if awk "BEGIN{exit !(${connect} > 0)}"; then
    echo "Gate 2 (connect): ok  — TCP connected in ${connect}s"
  else
    echo "Gate 2 (connect): FAILED — could not open a connection"; return
  fi
  # Gate 3: TLS handshake (only for https URLs).
  case "${url}" in
    https://*)
      if awk "BEGIN{exit !(${appconnect} > 0)}"; then
        echo "Gate 3 (TLS):     ok  — handshake done in ${appconnect}s"
      else
        echo "Gate 3 (TLS):     FAILED — certificate or handshake problem"; return
      fi ;;
    *) echo "Gate 3 (TLS):     n/a — not an https URL" ;;
  esac
  # Gate 4: status class.
  case "${code}" in
    2*) echo "Gate 4 (status):  ok  — HTTP ${code} (success)" ;;
    3*) echo "Gate 4 (status):  HTTP ${code} — a redirect; re-run with -L to follow it" ;;
    4*) echo "Gate 4 (status):  HTTP ${code} — CLIENT error: fix your request (URL, auth, or body)" ;;
    5*) echo "Gate 4 (status):  HTTP ${code} — SERVER error: the server failed a valid request" ;;
    *)  echo "Gate 4 (status):  HTTP ${code} — no valid response" ;;
  esac
}

main() {
  echo "Day 021 — Debug a Request with curl"
  echo "Test servers: ${EXAMPLE} and ${HTTPBIN}"
  if ! have_network; then
    echo
    echo "OFFLINE: could not reach ${EXAMPLE}. The commands below need network"
    echo "access. Connect to the internet and re-run: bash examples/debug_request.sh"
    exit 0
  fi
  show_verbose
  show_redirect
  show_timing
  show_headers_echo
  show_status_codes
  diagnose "${EXAMPLE}"
  diagnose "${HTTPBIN}/status/404"
  echo
  echo "Done. Every section above maps to a stage of the failed-request decision tree."
}

main "$@"
metadata.yml (593 bytes)
lesson_id: D021
day: 21
kind: api-example
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-021-inspecting-traffic-with-curl-and-developer
run_commands:
  - bash examples/debug_request.sh
  - bash starter/debug_request.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/debug_request.sh  # optional: reset your work'
requires_network: true
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (curl 8.7.1), online, bash tests/run_tests.sh → 15 checks, 0 failure(s), 0 skip(s)'
requirements/README.md (472 bytes)
# Dependencies — Day 021 lab

**`curl` only** — preinstalled on macOS and mainstream Linux (and available
on Windows 10+ / WSL). No package installs, no accounts, no API keys.

The lab reaches two free public test services — `https://example.com` and
`https://httpbin.org` — so it needs network access to show live output. With
no network it degrades gracefully: the scripts print a clear message and the
tests skip (never fail) the live checks. No sudo required.
starter/debug_request.sh (1879 bytes)
#!/usr/bin/env bash
# Day 021 starter — Debug a Request with curl.
# Complete the five exercises below. Each names the exact curl command to use.
# Run from the lab directory:  bash starter/debug_request.sh
# The completed reference is in ../examples/debug_request.sh — try it yourself first.
set -u

EXAMPLE="https://example.com"
HTTPBIN="https://httpbin.org"
rule() { printf '\n=== %s ===\n' "$1"; }

echo "Day 021 starter — Debug a Request with curl"

# Exercise 1: show the full conversation.
# Replace the echo with: curl -v -o /dev/null --max-time 15 "$EXAMPLE" 2>&1 | grep -E '^[*<>]'
rule "1. Verbose conversation"
echo "EXERCISE — your turn: run curl -v against $EXAMPLE and show the * < > lines"

# Exercise 2: follow a redirect.
# Replace with: curl -o /dev/null -sIL --max-time 25 -w '%{http_code}\n' "$HTTPBIN/redirect/1"
rule "2. Follow a redirect"
echo "EXERCISE — your turn: follow $HTTPBIN/redirect/1 with -L and print the final status"

# Exercise 3: timing breakdown.
# Replace with: curl -o /dev/null -s --max-time 15 -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} total:%{time_total}\n' "$EXAMPLE"
rule "3. Timing breakdown"
echo "EXERCISE — your turn: print the DNS/connect/TLS/total timings for $EXAMPLE"

# Exercise 4: echo the headers you sent.
# Replace with: curl -sf --max-time 25 --retry 4 -H "Accept: application/json" "$HTTPBIN/headers"
rule "4. Headers echo"
echo "EXERCISE — your turn: send an Accept header to $HTTPBIN/headers and show the echo"

# Exercise 5: read a deliberate 404 status code.
# Replace with: curl -o /dev/null -s --max-time 25 --retry 5 -w '%{http_code}\n' "$HTTPBIN/status/404"
rule "5. Read a status code"
echo "EXERCISE — your turn: print the status code returned by $HTTPBIN/status/404"

echo
echo "When all five are done, compare your output with ../examples/debug_request.sh"
starter/debug-worksheet.md (1024 bytes)
# Debug worksheet — Day 021

Run the commands (from the examples or your completed starter) and record what
YOUR machine and network returned. Values vary — that is expected.

## 1. A redirect you followed
- Command: `curl -IL https://httpbin.org/redirect/1`
- First response status: `____`  (should be a 3xx)
- Final status after `-L` follows it: `____`  (should be 200)

## 2. A timing breakdown
- Command: `curl -o /dev/null -s -w 'dns:%{time_namelookup} connect:%{time_connect} tls:%{time_appconnect} total:%{time_total}\n' https://example.com`
- DNS: `____`  connect: `____`  TLS: `____`  total: `____`
- Which stage took the longest? `____`

## 3. A status code you read deliberately
- Command: `curl -o /dev/null -s -w '%{http_code}\n' https://httpbin.org/status/503`
- Status returned: `____`  — which class is it, and what does it mean? `____`

## 4. One sentence
Using the decision tree (DNS → connect → TLS → status), describe how you would
diagnose a request that "just fails."

> _your answer here_
tests/run_tests.sh (4209 bytes)
#!/usr/bin/env bash
# Tests for the Day 021 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Two kinds of checks:
#   * Structure checks always run and must pass (files present, the example
#     script defines the expected sections, the starter names its exercises,
#     both scripts parse).
#   * Network checks run only when the internet is reachable. Offline, they
#     are SKIPPED with a message. When online but the public test server
#     (httpbin.org) is transiently unavailable, the affected check is also
#     SKIPPED rather than failed — an external outage is not the learner's bug.
#
# The script exits 0 when no structure check failed, whether online or offline.
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HTTPBIN="https://httpbin.org"
EXAMPLE="https://example.com"
failures=0
checks=0
skips=0

pass() { checks=$((checks + 1)); echo "  ok:   $1"; }
fail() { checks=$((checks + 1)); failures=$((failures + 1)); echo "  FAIL: $1"; }
skip() { skips=$((skips + 1)); echo "  skip: $1"; }

echo "== Structure checks =="
example="${lab_dir}/examples/debug_request.sh"
starter="${lab_dir}/starter/debug_request.sh"

[ -f "${example}" ] && pass "example script exists" || fail "example script missing"
[ -f "${starter}" ] && pass "starter script exists" || fail "starter script missing"
[ -f "${lab_dir}/starter/debug-worksheet.md" ] && pass "worksheet exists" || fail "worksheet missing"

# The example must exercise each core flag/section.
for needle in "curl -v" "curl -IL" "%{http_code}" "time_namelookup" "diagnose"; do
  if grep -q -- "${needle}" "${example}"; then pass "example uses '${needle}'"; else fail "example missing '${needle}'"; fi
done

# The starter must name its five numbered exercises.
ex_count="$(grep -cE 'Exercise [1-5]' "${starter}" 2>/dev/null || true)"
if [ "${ex_count:-0}" -ge 5 ]; then pass "starter has 5 numbered exercises"; else fail "starter has 5 numbered exercises (found ${ex_count:-0})"; fi

# Both scripts must be valid bash.
if bash -n "${example}" 2>/dev/null; then pass "example has valid bash syntax"; else fail "example has a syntax error"; fi
if bash -n "${starter}" 2>/dev/null; then pass "starter has valid bash syntax"; else fail "starter has a syntax error"; fi

echo
echo "== Network checks =="
if ! curl -s -o /dev/null --max-time 12 "${EXAMPLE}" 2>/dev/null; then
  skip "no network access — skipping all live-request checks (expected offline)"
else
  pass "network reachable (${EXAMPLE} responded)"

  # Check 1: curl follows a redirect through to a final 200 (redirect/1 -> /get).
  final="$(curl -o /dev/null -sIL --max-time 25 --retry 5 --retry-delay 1 \
    -w '%{http_code}' "${HTTPBIN}/redirect/1" 2>/dev/null)" || final="000"
  case "${final}" in
    200) pass "curl -L followed the redirect to a final HTTP 200" ;;
    5* | 000) skip "redirect check — httpbin transiently unavailable (got '${final}'); retry later" ;;
    *) fail "curl -L should reach HTTP 200 (got '${final}')" ;;
  esac

  # Check 2: reads a 404 status correctly. --retry rides past transient 5xx;
  # a real 404 is not a transient error, so curl returns it without retrying.
  code404="$(curl -o /dev/null -s --max-time 25 --retry 5 --retry-delay 1 \
    -w '%{http_code}' "${HTTPBIN}/status/404" 2>/dev/null)" || code404="000"
  case "${code404}" in
    404) pass "curl read the deliberate 404 status correctly" ;;
    5* | 000) skip "404 check — httpbin transiently unavailable (got '${code404}'); retry later" ;;
    *) fail "curl should read HTTP 404 (got '${code404}')" ;;
  esac

  # Check 3: the headers endpoint echoes back a header we set with -H.
  echoed="$(curl -sf --max-time 25 --retry 5 --retry-delay 1 \
    -H "X-Day-21-Test: inspecting" "${HTTPBIN}/headers" 2>/dev/null)" || echoed=""
  if [ -z "${echoed}" ]; then
    skip "headers-echo check — httpbin transiently unavailable; retry later"
  elif printf '%s' "${echoed}" | grep -q "X-Day-21-Test"; then
    pass "the headers endpoint echoed back the header sent with -H"
  else
    fail "the headers endpoint should echo the -H header we sent"
  fi
fi

echo
echo "${checks} checks, ${failures} failure(s), ${skips} skip(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 021 lab

httpbin.org returns 503 / times out

httpbin.org is a shared free service and sometimes returns gateway errors under load. The example script already retries transient 5xx and, for the headers demo, falls back cleanly. If it keeps failing, wait a minute and re-run, or point the command at https://example.com (for the non-echo checks). A 503 is itself a valid teaching case — it is a real server error.

curl -v prints a lot — where is the request vs the response?

Lines starting with * are connection notes, > are the request headers you sent, and < are the response headers you got back. The example filters to just those with grep -E '^[*<>]'.

A redirect doesn't seem to work

Use -L to make curl follow the Location header; without it curl stops at the 3xx and shows only the first response. curl -IL <url> follows redirects and shows headers only.

The timings look tiny or zero

curl -w reports cumulative seconds from the start: time_appconnect (TLS done) ≥ time_connect (TCP done) ≥ time_namelookup (DNS done). On a warm connection these can all be small — that is normal.

Offline

The scripts and tests detect no network and exit 0 with a clear message. Connect to the internet to see live output.

Security notes

Security notes — Day 021 lab

  • What it does: sends ordinary GET requests to two public test services (example.com, httpbin.org) and reads the responses. No data of yours is sent beyond a normal request; no sudo; no files written outside the lab.
  • Never send real secrets to test services. httpbin.org echoes back whatever you send. The lab uses only non-sensitive sample values. Do not post real API keys, tokens, or passwords to any echo service.
  • curl -v output can contain sensitive headers (cookies, Authorization) if you point it at a site you are logged into. Redact those before sharing a capture. The lab targets only anonymous public endpoints.
  • Only inspect hosts you are allowed to. Probing or scanning servers you do not own or have permission to test can be against their terms or the law.