diff --git a/README.md b/README.md index ec857e8..a64d869 100644 --- a/README.md +++ b/README.md @@ -413,10 +413,12 @@ Local hosts are tracked separately from WAN stats. ### Latency measurement -GET requests to each target's URL as written, with `mode: 'no-cors'` and -`cache: 'no-store'`, timed with `performance.now()`. No query string is added: -the Hetzner speed-test servers close the connection without an answer when the -URL has one, and `no-store` keeps the browser's cache out of the measurement. +GET requests to each target's URL as written, with `mode: 'no-cors'`, +`cache: 'no-store'` and `referrerPolicy: 'no-referrer'`, timed with +`performance.now()`. The Hetzner speed-test servers close the connection without +an answer when the URL has a query string or the request has a `Referer` header. +So no query string is added, and no `Referer` header is sent, wherever the page +is served from. `no-store` keeps the browser's cache out of the measurement. Each check times out after 80% of the refresh interval (24 seconds at 30 seconds) and is then recorded as a timeout, so a round's checks have all finished before the next round is due. When no WAN host answers, a recovery diff --git a/TODO.md b/TODO.md index 82bb8a0..2bb64ce 100644 --- a/TODO.md +++ b/TODO.md @@ -23,6 +23,12 @@ Run `make frontend-viewport-test` in CI as its own step; it is not part of # Completed Steps +- 2026-10-26: the six Hetzner targets answer wherever the page is served from + ([#191](https://git.eeqj.de/sneak/netwatch/issues/191)). They close the + connection without an answer when the request has a `Referer` header. The + browser sent one unless the page came with the image's + `Referrer-Policy: no-referrer` header, which a static host and `make dev` do + not send. Each check now sets `referrerPolicy: 'no-referrer'` - 2026-10-19: the server's `OnStart` hook in `internal/server/server.go` starts `listenAndServe` with `go func() { s.listenAndServe() }()`, as the Go style guide asks ([#162](https://git.eeqj.de/sneak/netwatch/issues/162)); it was diff --git a/test/unit/main.test.js b/test/unit/main.test.js index 466204e..c10a492 100644 --- a/test/unit/main.test.js +++ b/test/unit/main.test.js @@ -180,6 +180,17 @@ test("a check fetches the target's URL as written, with no query string added", ); }); +test("a check sends no Referer header, wherever the page is served from", async (t) => { + mockTargets(t, () => 10); + const check = measureLatency("https://fsn1-speed.hetzner.com"); + t.mock.timers.tick(10); + assert.notEqual(await settled(check), "still waiting"); + assert.equal( + fetch.mock.calls[0].arguments[1].referrerPolicy, + "no-referrer", + ); +}); + test("at a 30000ms interval, a target answering after 1000ms shows in its row while another target's check is still waiting", async (t) => { CONFIG.updateInterval = 30000; const state = new AppState([ diff --git a/web/src/main.js b/web/src/main.js index 78746da..99eacd9 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -572,10 +572,11 @@ export class Reporter { // it as a timeout. A failed check's reason says what went wrong and after // how long; a check that answered has none. // -// The URL is fetched as written, with nothing added to it: the Hetzner -// speed-test servers close the connection without an answer when the URL -// has a query string, and cache: "no-store" keeps the browser's cache out -// of the measurement. +// The Hetzner speed-test servers close the connection without an answer +// when the URL has a query string or the request has a Referer header. So +// the URL is fetched as written, and referrerPolicy: "no-referrer" sends +// no Referer header, wherever the page is served from. cache: "no-store" +// keeps the browser's cache out of the measurement. export async function measureLatency(url, signal) { const controller = new AbortController(); const timeoutId = setTimeout( @@ -591,6 +592,7 @@ export async function measureLatency(url, signal) { method: "GET", mode: "no-cors", cache: "no-store", + referrerPolicy: "no-referrer", signal: controller.signal, }); const latency = Math.round(performance.now() - start);