Syllabus Lesson 38 of 156 · HTTP from the inside
HTTP from the inside

Redirect chains

Why it matters A single click can pass through two or three servers before you land, in a fraction of a second, and each one gets a full, first-party look at you: its own cookies, where you came from, and whatever the link carried. Logging the hops shows exactly who learned what, which is the evidence a bounce-tracking defence is built on.

Imagine posting a letter that is opened, read and re-sealed at two sorting offices you never chose, each adding a note for the next. You only see it arrive. A redirect chain is that journey, and your job here is to read the postmarks.

Redirect: a response with a 3xx status (301, 302, 303, 307 or 308) and a Location header saying where to go instead. The browser follows it by itself, without asking you.

Redirect chain: the list of addresses a navigation passed through, ending at the page you see. In the practice world this is visit.chain.

Bounce tracking: sending you through a tracker's site in a quick redirect. Because you are, for a moment, visiting the tracker directly, it can read and set its own first-party cookies even where third-party cookies are blocked.

The practice chain

bounce_world() from watched.leg2c_http adds a link shortener, lnk.example. Sam clicks https://lnk.example/s/deal on ShopA. Then:

  1. lnk.example answers 301 to an AdCo click link and sets its own lnk_seen cookie.
  2. adco.example/click reads or sets adco_id, then answers 302 to ShopB with adco_uid=<the same ID> added to the address. That is link decoration from the last lesson.
  3. shopb.example answers 200, and receives AdCo's ID in its own address bar.

hop_exchanges(visit) gives you the raw request and response for each hop, as you would copy them from DevTools with "Preserve log" switched on. You already have every parser you need.

What each hop learns

  • Cookies: who you are to that hop. On the second click, both bouncers recognise Sam.
  • Referer: where the click started (here, ShopA's origin, kept by the browser for every hop of the chain).
  • Query parameters: whatever the previous hop chose to pass on. This is how ShopB ends up holding AdCo's ID.
  • Set-Cookie: what the hop asked the browser to remember for next time.

seen_by(log, value) then answers the key privacy question: which servers received this ID? If the answer includes a site you only meant to visit, the ID crossed a site boundary.

What browsers do about it (checked October 2026)

Safari has detected sites that only ever appear in the middle of chains, never as a destination you interact with, and cleared their storage since 2018. Firefox purges known trackers' storage daily by default and has fuller bounce tracking protection in Strict mode. Chrome applies its bounce tracking mitigations only for people who block third-party cookies. You will build that detector yourself in the blocker module.

A common misconception

"A redirect is too quick to matter." Speed is irrelevant to the server: it receives a full request with cookies and a Referer, and can answer with new cookies, all before you see anything. The only trace on your side is the chain, which is why you log it.

Try it In DevTools, Network tab, tick "Preserve log", then click a shortened link or an ad link in a search result. You will see the 301 or 302 hops before the final page. Click each one and compare its Request Headers: Cookie, Referer and the query string.

In other languages

JavaScript:

// fetch can show one hop at a time if you stop it following redirects (in Node)
const res = await fetch(url, { redirect: "manual" });
console.log(res.status, res.headers.get("location"), res.headers.getSetCookie());

Go:

client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
    fmt.Println("hop:", req.URL, "from:", req.Referer(), "hops so far:", len(via))
    return nil
}}

Recap

  • Each redirect hop is a full first-party request: the server sees its own cookies, the Referer and the query.
  • Log hops from the raw exchanges with the parsers you already wrote: host, status, Referer, cookies, params, Set-Cookie names, Location.
  • Following one ID through the log shows which sites learned it; an ID reaching a site you did not choose is the sign of bounce tracking and decoration.
Your turn

parse_message, first and parse_cookie_header are given. Write log_hops(exchanges): for each (raw_request, raw_response) pair, a dict with "host", "status" (an int), "referer" (or None), "cookies" (a dict), "params" (the query as a dict via parse_qsl(..., keep_blank_values=True)), "set_cookies" (the names set, in order) and "location" (or None). Then write seen_by(log, value): the hosts, in order and without repeats, whose request carried value as a parameter value, as a cookie value, or inside the Referer.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output