clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / docs / decisions / 0017-the-host-follows-redirects.md
0017-the-host-follows-redirects.md 3.2K rendered
1# 17. The desktop host follows the application's redirects23- **Date:** 2026-08-024- **Status:** accepted5- **Where it is written down:** [app.md](../app.md) §1.4; `redirects.go`; issue #5 §3.4, §4.267## Context89The application gives a world exactly one URL and treats `/` as a doorway: it10answers `GET /` with `302 Found` and a `Location` naming the volume and world11the reader was last in ([app.md](../app.md) §2.2). `/open` does the same for the12topbar's selects, which cannot build a path out of their own values. Under13`atlas serve`, a browser follows those redirects like any other and lands at14the real URL, which is the whole point of having one.1516Under Wails there is no browser fetch and no socket. The page is served over a17custom URL scheme, and a scheme task — `WKURLSchemeTask` on macOS, and its18equivalents on WebKitGTK and WebView2 — has no way to express a redirect. The19platform hands the `302` to the page as though it were a document. The first20launch of the desktop host showed the reader a white page reading **"Found."**,21which is the body Go writes in a redirect nobody was ever meant to read.2223Three answers were available: make the handler stop redirecting, make the shell24mount a different handler, or resolve the redirect in the host.2526## Decision2728**The host follows them.** `redirects.go` wraps the handler before it is handed29to `wails.Run`: a GET whose answer is a 3xx with a `Location` naming a path is30re-issued against the same handler, up to five hops, and what comes back is31what the page receives.3233The handler is not told. `atlas serve` goes on serving those redirects to34clients that follow them, the recorded HTTP transcript goes on judging what the35handler sends, and the two hosts differ in the transport rather than in the36application.3738Three properties keep the wrapper honest:3940- **GET only.** Only a navigation is redirected here; a POST answers with a41 partial.42- **Same application only.** A `Location` that does not begin with `/` is43 passed to the page untouched. The application never writes one, and if44 something ever does, it is the page's business and not the host's.45- **Streaming survives.** The wrapper keeps its own header map only until the46 status is known; from the first non-redirect status the bytes go straight to47 the real writer, and `Unwrap` hands `http.ResponseController` the flusher48 underneath. The events stream and the import's rows stream as they always49 did.5051## Consequences5253- The rule "everything OS-shaped lives behind hostenv" gains a sibling: *a54 limitation of the host's transport is answered in the host.* This is the55 first instance, and it is the shape any future one should take.56- The WASM service-worker host of [ADR 7](0007-hostenv-portability.md) will not57 need this: a `Response` from a service worker carries a redirect properly.58 The wrapper is the webview's, not the portability layer's.59- A redirect loop in the application shows up as `508 Loop Detected` in the60 window rather than as a hang. Two hops is the most this application can61 need; the other three are headroom.62- The finding itself is the argument for launching the thing. It is invisible63 to `go test`, to the HTTP goldens and to `atlas serve`, and it made the64 application's front door unusable.