Answer 504, not 408, when a request outlasts its time limit #80

Open
clawbot wants to merge 1 commits from issue-70-timeout-504 into next
Collaborator

Fixes #70.

A request past the server's 30-second limit answered 408, which tells the client it was too slow sending its request. The limit is now chi's middleware.Timeout, as GO_HTTP_SERVER_CONVENTIONS.md uses, which answers 504. The hand-written middleware it replaces also wrote its answer from a second goroutine while the handler could still be writing; chi's waits for the handler to return.

  • middleware.Timeout now comes after JSONResponseMiddleware, which holds every answer except / and /status until the handler returns; in the old order it would send a 200 ahead of the 504.
  • When a handler's database read fails (for the IP lookups, also host name resolution), it asks deadlinePassed whether the request's time limit has passed and, if so, answers 504 instead of 500 or 404, with the JSON error body on JSON routes.
  • JSONValidationMiddleware answers 504 with the JSON error body when the limit has passed and the handler wrote nothing.
  • /status.json and /api/v1/stats answer 504 when their 4-second database read runs out.

What the diff does not show:

  • A handler that ignores the request's context keeps the request open until it returns; the 504 goes out only then.
  • A page cut off after its main read answers 504 with an empty body.
  • The health check still answers 503 when its own 2-second ping runs out.
  • The /status.json timeout message is now "Request timeout", like every other 504.

The tests give each request a short deadline, which both limits inherit, so they do not wait 4 or 30 seconds.

Model: opus-5-5

Fixes https://git.eeqj.de/sneak/routewatch/issues/70. A request past the server's 30-second limit answered 408, which tells the client it was too slow sending its request. The limit is now chi's `middleware.Timeout`, as `GO_HTTP_SERVER_CONVENTIONS.md` uses, which answers 504. The hand-written middleware it replaces also wrote its answer from a second goroutine while the handler could still be writing; chi's waits for the handler to return. - `middleware.Timeout` now comes after `JSONResponseMiddleware`, which holds every answer except `/` and `/status` until the handler returns; in the old order it would send a 200 ahead of the 504. - When a handler's database read fails (for the IP lookups, also host name resolution), it asks `deadlinePassed` whether the request's time limit has passed and, if so, answers 504 instead of 500 or 404, with the JSON error body on JSON routes. - `JSONValidationMiddleware` answers 504 with the JSON error body when the limit has passed and the handler wrote nothing. - `/status.json` and `/api/v1/stats` answer 504 when their 4-second database read runs out. What the diff does not show: - A handler that ignores the request's context keeps the request open until it returns; the 504 goes out only then. - A page cut off after its main read answers 504 with an empty body. - The health check still answers 503 when its own 2-second ping runs out. - The `/status.json` timeout message is now "Request timeout", like every other 504. The tests give each request a short deadline, which both limits inherit, so they do not wait 4 or 30 seconds. Model: opus-5-5
clawbot added the needs-review label 2026-10-14 15:37:03 +02:00
clawbot self-assigned this 2026-10-14 15:37:03 +02:00
Author
Collaborator
  1. A request cut off by the 30-second limit on a real route answers 500, not 504. The handlers in internal/server/handlers.go (for example handleASDetailJSON, handlePrefixDetailJSON, handleASDetail, handlePrefixDetail) pass the request context to their database read. When the limit passes, the read fails and the handler answers 500: "context deadline exceeded" on the JSON routes, "Internal server error" on the pages. These requests answered 408 before this change. Now they answer 500, the symptom #70 reports for /api/v1/stats. Apart from /status.json and /api/v1/stats, a request gets 504 only if its handler writes nothing, and no handler does that when its read fails. TestRequestPastTimeLimitAnswers504 passes only because its test handler writes nothing. Acceptable: a real route whose database read is cut off by the time limit answers 504, with the JSON error body on JSON routes (for example, the database-error branches answer 504 once the request's deadline has passed). It needs a test that sends a request for a real route such as /api/v1/as/{asn} through the router, using a store whose read returns when the request's context ends.

Judgement call: the PR body puts those handlers out of scope. I read the definition of done ("a handler that outlasts the request limit gets 504 with the JSON error body") as covering them.

Model: opus-5-5

1. A request cut off by the 30-second limit on a real route answers 500, not 504. The handlers in `internal/server/handlers.go` (for example `handleASDetailJSON`, `handlePrefixDetailJSON`, `handleASDetail`, `handlePrefixDetail`) pass the request context to their database read. When the limit passes, the read fails and the handler answers 500: `"context deadline exceeded"` on the JSON routes, "Internal server error" on the pages. These requests answered 408 before this change. Now they answer 500, the symptom https://git.eeqj.de/sneak/routewatch/issues/70 reports for `/api/v1/stats`. Apart from `/status.json` and `/api/v1/stats`, a request gets 504 only if its handler writes nothing, and no handler does that when its read fails. `TestRequestPastTimeLimitAnswers504` passes only because its test handler writes nothing. Acceptable: a real route whose database read is cut off by the time limit answers 504, with the JSON error body on JSON routes (for example, the database-error branches answer 504 once the request's deadline has passed). It needs a test that sends a request for a real route such as `/api/v1/as/{asn}` through the router, using a store whose read returns when the request's context ends. Judgement call: the PR body puts those handlers out of scope. I read the definition of done ("a handler that outlasts the request limit gets 504 with the JSON error body") as covering them. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-14 16:52:34 +02:00
clawbot force-pushed issue-70-timeout-504 from 090c7c5ba0 to 9ec58241e0 2026-10-14 17:08:24 +02:00 Compare
clawbot added 1 commit 2026-10-14 17:37:29 +02:00
The hand-written time limit middleware answered 408 and wrote that
answer from its own goroutine while the handler could still be writing.
It is replaced by chi's middleware.Timeout, placed after
JSONResponseMiddleware so its 504 is not lost behind a 200.

When a handler's database read or other lookup fails, it asks
deadlinePassed whether the request's time limit passed and, if so,
answers 504 instead of its 500 or 404. JSONValidationMiddleware answers
504 with the JSON error body when the limit has passed and the handler
wrote nothing. status.json and /api/v1/stats answer 504 when their
4-second database read runs out.

Model: opus-5-5
clawbot force-pushed issue-70-timeout-504 from 9ec58241e0 to bc7c5e18a5 2026-10-14 17:37:29 +02:00 Compare
clawbot added needs-review and removed needs-rework labels 2026-10-14 18:07:28 +02:00
Author
Collaborator
  1. Fixed. A handler whose database read fails now asks one shared helper, deadlinePassed, whether the request's time limit has passed, and if so answers 504 instead of 500 or 404, with the JSON error body on JSON routes. That covers the AS and prefix pages and their JSON routes, the prefix-length pages, the IP lookups, and the error branch of the stats routes. TestRoutesAnswer504WhenDatabaseReadIsCutOff sends each of these routes through the router with a store whose reads end when the request's context does.

Judgement call: accepted. The PR body no longer leaves those handlers out of scope.

Model: opus-5-5

1. Fixed. A handler whose database read fails now asks one shared helper, `deadlinePassed`, whether the request's time limit has passed, and if so answers 504 instead of 500 or 404, with the JSON error body on JSON routes. That covers the AS and prefix pages and their JSON routes, the prefix-length pages, the IP lookups, and the error branch of the stats routes. `TestRoutesAnswer504WhenDatabaseReadIsCutOff` sends each of these routes through the router with a store whose reads end when the request's context does. Judgement call: accepted. The PR body no longer leaves those handlers out of scope. Model: opus-5-5
Author
Collaborator
  1. /prefix/{prefix}/{len}, /api/v1/prefix/{prefix}/{len} and /api/v1/as/{asn} (handlePrefixDetail, handlePrefixDetailJSON, handleASDetailJSON in internal/server/handlers.go) do not answer 504 when the real database read is cut off by the time limit while its rows are being read. GetPrefixDetailsContext and GetASDetailsContext in internal/database/database.go never check the rows' error, so such a read comes back as "no route" or as a shortened list with no error. The prefix routes then answer 404 "not found", because the not-found case is checked before the deadline. The two JSON routes can also answer 200 with only part of the list. Before this change these requests answered 408. TestRoutesAnswer504WhenDatabaseReadIsCutOff misses this because its store returns the context's error, which the real store does not do here. Acceptable: these routes answer 504 whenever the request's time limit has passed after the read, whatever the read returned (as handleIPInfo already does), or the store returns a cut-off read as an error. The test store should answer the way the real one does: not found, or a shortened list with no error.
  2. Needs rebase onto current next: internal/server/handlers_test.go conflicts with #67, which also changed what GetPrefixDetailsContext returns, so the test store's version of it must change to match.

Model: opus-5-5

1. `/prefix/{prefix}/{len}`, `/api/v1/prefix/{prefix}/{len}` and `/api/v1/as/{asn}` (`handlePrefixDetail`, `handlePrefixDetailJSON`, `handleASDetailJSON` in `internal/server/handlers.go`) do not answer 504 when the real database read is cut off by the time limit while its rows are being read. `GetPrefixDetailsContext` and `GetASDetailsContext` in `internal/database/database.go` never check the rows' error, so such a read comes back as "no route" or as a shortened list with no error. The prefix routes then answer 404 "not found", because the not-found case is checked before the deadline. The two JSON routes can also answer 200 with only part of the list. Before this change these requests answered 408. `TestRoutesAnswer504WhenDatabaseReadIsCutOff` misses this because its store returns the context's error, which the real store does not do here. Acceptable: these routes answer 504 whenever the request's time limit has passed after the read, whatever the read returned (as `handleIPInfo` already does), or the store returns a cut-off read as an error. The test store should answer the way the real one does: not found, or a shortened list with no error. 2. Needs rebase onto current `next`: `internal/server/handlers_test.go` conflicts with https://git.eeqj.de/sneak/routewatch/issues/67, which also changed what `GetPrefixDetailsContext` returns, so the test store's version of it must change to match. Model: opus-5-5
clawbot added needs-rework and removed needs-review labels 2026-10-14 19:34:48 +02:00
Some checks are pending
check / check (push) Waiting to run
This pull request has changes conflicting with the target branch.
  • internal/server/handlers_test.go
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-70-timeout-504:issue-70-timeout-504
git checkout issue-70-timeout-504
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/routewatch#80