11 Commits
Author SHA1 Message Date
sneak 71b8d46f7b server: assert timeouts on the served http.Server, not just the constructor (closes #120)
check / check (push) Failing after 1s
The timeout tests called newHTTPServer directly, leaving Run's call
site unguarded: a Run that built its http.Server inline would drop
every timeout with the suite still green. TestRunWiresSocketTimeouts
now wires a Server as cmd/dnswatcher does (minus watcher and resolver,
so no live DNS), drives Run with an unbindable port so it stores its
http.Server and returns without listening, and asserts the served
server carries all four timeouts and both required relationships.
Reverting Run to an inline http.Server without timeouts fails it. The
near-tautological addr/handler test is dropped; the wiring test covers
the handler end to end.

The read-deadline note is corrected: a too-small ReadTimeout does not
make ReadHeaderTimeout unreachable (net/http applies it directly); it
installs an already-expired whole-request deadline. Verified against
the pinned go1.25 net/http.

Model: opus-4-8
2026-09-21 07:51:28 +00:00
clawbot b351a2350c gomod: drop stale golang.org/x/sync indirect line (closes #132)
check / check (push) Failing after 0s
go.mod listed golang.org/x/sync v0.19.0 twice: once in the direct
require block and once as // indirect. The module is a genuine direct
dependency (internal/portcheck/portcheck.go imports
golang.org/x/sync/errgroup), so the indirect entry was redundant and
stale. script/bootstrap ends with go mod download, which dropped that
line as a side effect and left every fresh checkout with a dirty
go.mod. Running go mod tidy removes the line for good; no dependency
and no version changed, and go.sum is unaffected.

Model: opus-4-8
2026-09-21 09:44:49 +02:00
clawbot 1ffe303a6e Merge main into next, resolving TODO.md (closes #145)
check / check (push) Failing after 1s
The last milestone landed on main as a squash, so main was no longer an
ancestor of next and git merge-tree reported a TODO.md conflict: both
branches added the same block of Completed Steps entries, and next added
one more (the #99 http.Server timeouts entry). This real merge commit
makes main an ancestor of next again. TODO.md is resolved by hand to keep
every entry from both sides exactly once, which is next's version since
next's entries are a superset of main's. Every other file already equals
next. No new TODO.md entry is added, per the task brief.

Model: opus-4-8
2026-09-21 07:22:17 +00:00
clawbot fc43f893a5 server: set all four http.Server socket timeouts (#118)
check / check (push) Successful in 3m50s
The http.Server was built with only ReadHeaderTimeout set; the other three
timeouts were zero, which in net/http means no limit, so a peer could hold a
connection open past the header phase, responses had no write deadline, and
keep-alive connections were never reaped. ReadTimeout 15s, WriteTimeout 75s,
and IdleTimeout 120s now join ReadHeaderTimeout 10s as named constants.
WriteTimeout must stay above the 60s chimw.Timeout handler budget, because
net/http arms the write deadline once request headers are read; a test fails
the build if either number moves alone. The server literal moved into
newHTTPServer so the configuration can be asserted without binding a socket
(closes #99)

Model: opus-5
2026-09-09 15:17:43 +02:00
clawbot ae06f7e3a1 notify: drain in-flight deliveries at shutdown (closes #106)
check / check (push) Successful in 1m22s
Shutdown waits for deliveries already in flight instead of dropping them.

Reviewed and green; squashed to next by the dispatcher, dnswatcher having no manager.

Model: opus-5
2026-09-09 14:26:53 +02:00
sneak b8662b8a9c docs: correct stale script headers and record the config-verify cost (closes #137)
check / check (push) Successful in 51s
script/bootstrap credited the goimports pin to script/fmt-check, which
runs gofmt only; the header now credits script/fmt. script/cibuild
still described the Dockerfile as running make check, which stopped
being true once linting moved to its own stage. The docker-missing
warning in script/bootstrap is one sentence instead of three fragments
each carrying the bootstrap: prefix. Dockerfile.lint now states the
residual risk of skipping golangci-lint config verify: unknown
top-level keys in .golangci.yml are ignored silently, so a mistyped key
lints clean and applies nothing.

Comment and message text only; no behaviour changes.
2026-08-10 14:09:20 +00:00
sneak 168281ad60 docs: add MIT LICENSE and state the licence in the README (closes #102)
check / check (push) Has been cancelled
The repository had no licence file at all, which makes publicly readable
code all-rights-reserved by default: nobody may legally use it. That is a
1.0 blocker rather than a nicety, and `LICENSE` was the only file from
`REPO_POLICIES.md`'s required minimum still missing here.

The choice is standing org policy rather than a per-repo call: any public
repo lacking a licence gets MIT, while a private repo with no licence is
already all-rights-reserved and needs nothing. `sneak/dnswatcher` is
public, so MIT.

`LICENSE` carries the canonical MIT text byte-for-byte with only the
copyright line filled in; no clauses added, removed, reworded, or
reflowed. `README.md`'s first line now names the licence, which the
Description requirement in `REPO_POLICIES.md` calls for, and the License
section states MIT and points at the file instead of recording the
decision as pending.
2026-08-10 14:04:31 +00:00
sneak 6f6bf3a65b test: disable Go's test cache so every run queries live DNS (closes #139)
check / check (push) Successful in 1m34s
`script/test` did not pass `-count=1`, so on an unchanged tree Go
served the whole suite from its test cache: exit 0 in ~0.2s with every
package marked `(cached)` and not one DNS query made. This repo's suite
exists to exercise live resolution on every run (`TESTING.md`), so that
green asserted nothing — and it is exactly the green used as evidence
that a flakiness fix works, since "run it a few times" stops being
runs after the first.

`-count=1` now disables caching on every invocation.

The conditional verbose rerun that `REPO_POLICIES.md` mandates was
missing at the same spot and is added here rather than left broken: the
primary run had been unconditionally `-v`, which is the failure mode
the policy exists to prevent (unreadable CI and `docker build` logs on
success). Tests now run quiet, and only a failure triggers the `-v`
rerun. The rerun carries `-count=1` too, so it cannot replay a cached
copy of the failure it is meant to diagnose, and its exit status is
discarded in favour of a forced 1: the first failure already proved the
suite broken, so a flake that passes the second time must not turn the
build green.

`-timeout 90s` is untouched. It is a deliberate backstop that must
strictly exceed the 60s hard cap on suite duration.

No special-casing for the Docker build, which also reaches this script
via `RUN make test`: a fresh container's test cache is empty, so
`-count=1` changes nothing there and carving out an exception would
only create a second code path that could drift.

Verified: three back-to-back `make test` runs on an unchanged tree,
zero `(cached)` markers, ~4.0-4.5s wall each (was ~0.2s cached),
comfortably inside the 20s target with `-race` and `-cover` both still
working and coverage percentages unchanged. The rerun-and-still-fail
path was exercised against a purpose-built flaky test that fails once
then passes: quiet failure, verbose rerun that genuinely re-executed,
exit 1 regardless. `make check` green.
2026-08-10 13:48:15 +00:00
sneak 87bce43f8d test: rework live-DNS quorum unit — tolerate silence, never a wrong answer
check / check (push) Successful in 1m23s
Rework of the unit at #93
(commit 9cb2c2b), against the review at
#136 (comment).

Review of 9cb2c2b found the quorum assertions could not fail on a
class of wrong answer. Each test banned exactly one bad status —
_AllReturnOK banned only nxdomain, _NXDomainFromAllNS banned only ok
— so resolver.StatusNoData passed both. nodata is a wrong answer, not
silence, and answeredCount counted it as answered, so it did not even
trigger a retry; with a quorum of 3 of 4 a single wrong nameserver
slid through undetected. That is assertion-loosening beyond what the
quorum change requires.

Tolerance is now a closed allowlist rather than a blocklist of one
status. unsanctionedStatuses() reports every per-nameserver result
whose status the caller did not explicitly sanction: ok/timeout/error
for the all-OK test, nxdomain/timeout/error for the NXDOMAIN test.
Silence (timeout, error) is the only thing quorum exists to tolerate;
any other status, including one added to the resolver later, fails by
name. answeredCount is likewise an allowlist of ok/nxdomain/nodata, so
an unknown status counts as silence and can only cause a retry and
then a loud failure, never a quiet pass.

Two harness tests cover the regression directly: three OK plus one
nodata (quorum satisfied, no nxdomain present — the input that used
to pass) is now reported as unsanctioned, and an unknown status is
neither counted as answered nor tolerated.

Verified by re-running the reviewer's probe: queryEachNS patched to
force one of google.com's four nameservers to return StatusNoData
turns both tests red, naming the offending nameserver and status —

    --- FAIL: TestQueryAllNameservers_AllReturnOK (1.12s)
        Should be empty, but was [ns1.google.com.=nodata]
        every nameserver must answer OK or not answer at all:
        ns1.google.com.=nodata ns2.google.com.=ok ns3.google.com.=ok
        ns4.google.com.=ok
    --- FAIL: TestQueryAllNameservers_NXDomainFromAllNS (1.34s)
        Should be empty, but was [ns1.google.com.=nodata]
        every nameserver must report NXDOMAIN or not answer at all:
        ns1.google.com.=nodata ns2.google.com.=nxdomain
        ns3.google.com.=nxdomain ns4.google.com.=nxdomain

— and green with the probe reverted. Also fixes the review's nit: the
per-attempt deadline assertion had no lower bound, so it passed for a
deadline far shorter than intended.

No production code changed; DNS is still never mocked.
2026-08-10 13:33:53 +00:00
sneak 9cb2c2b7e0 test: make live DNS tests robust instead of gated (closes #93)
check / check (push) Successful in 1m18s
The resolver's live-DNS tests failed nondeterministically, a different
subset each run. Three structural causes, all test-side:

- Burst fan-out. Every test in the package is parallel and the build
  hosts have many cores, so all ~35 iterative resolutions started at
  the same instant and, because queryServers walks rootServerList() in
  fixed order, hit the same root server within milliseconds. Root
  servers rate-limit that.
- No retry anywhere. One dropped UDP packet in a delegation chain
  failed a test outright.
- Unanimity assertions. TestQueryAllNameservers_AllReturnOK and
  _NXDomainFromAllNS required every one of a domain's nameservers to
  answer, with no tolerance for one being slow.

New internal/resolver/livedns_test.go addresses each: a package-wide
gate bounds how many live resolutions are in flight at once, every
live operation gets three attempts with exponential backoff and its
own deadline, and multi-nameserver assertions now need a strict
majority rather than unanimity. The retry predicate is deliberately
transport-level -- "did a nameserver answer at all" -- never the
assertion under test, so a resolver that answers incorrectly still
fails on the first attempt. A nameserver that stays silent is
tolerated; one that answers wrongly is not.

livedns_harness_test.go tests that machinery directly: quorum
arithmetic, status counting, the gate's concurrency bound, per-attempt
deadlines, and recovery from a transient failure. It touches no DNS.

Nothing is mocked, faked, stubbed, recorded, skipped or build-tagged,
and production resolver behaviour is unchanged.

Test caps move to the new org-wide values ruled at prompts issue 41:
60s hard cap, 20s target, 90s -timeout backstop. REPO_POLICIES.md is
re-vendored byte-identical from sneak/prompts rather than hand-edited,
which also picks up the golangci-lint paragraph this copy had drifted
behind on. TESTING.md's stale 30-second target follows to 60.

#93
2026-08-10 13:09:17 +00:00
sneak cc86473410 build: run all linting in Docker via Dockerfile.lint (closes #134)
check / check (push) Successful in 1m17s
golangci-lint is no longer installed or run on the host. script/lint is
now a thin wrapper that builds the new root Dockerfile.lint, which COPYs
the repo into the digest-pinned golangci/golangci-lint:v2.12.2 image and
lints as a build step, so a successful build is a clean lint. This works
even where the docker daemon is remote and bind mounts are impossible.

Dockerfile.lint is split into a deps stage (base image, go mod download)
and a lint stage (source copy, linter run). script/lint passes
--no-cache-filter=lint so the lint stage executes on every invocation:
caching is explicitly waived for linting, and a cached build lints
nothing. The deps stage stays cached and no global cache invalidation is
performed. --progress=plain keeps the linter's own output visible.

golangci-lint config verify is deliberately omitted: it fetches its JSON
schema over a live, unpinned HTTPS call, which would make linting
network-dependent and defeat hash-pinning.

script/bootstrap no longer installs golangci-lint and warns instead when
docker is absent. The goimports install stays, since script/fmt and
script/fmt-check still run it on the host.

The root Dockerfile ran make check in its builder stage, which would now
recurse into script/lint and shell out to docker build with no daemon
available. It gains its own lint stage on the same pinned image, invoked
directly, with the builder depending on it via COPY --from=lint and
running make fmt-check, make test and make build.
2026-08-10 12:37:47 +00:00
6 changed files with 260 additions and 8 deletions
+21
View File
@@ -182,6 +182,27 @@ dnswatcher exposes a lightweight HTTP API for operational visibility:
| `GET /api/v1/status` | Current monitoring state | | `GET /api/v1/status` | Current monitoring state |
| `GET /metrics` | Prometheus metrics (optional) | | `GET /metrics` | Prometheus metrics (optional) |
#### Server timeouts
The HTTP server sets all four socket-level timeouts. These are compile-time
constants in `internal/server/server.go`, not configurable via environment
variables.
| Timeout | Value | Purpose |
|---------------------|-------|-----------------------------------------------|
| `ReadHeaderTimeout` | 10s | Bounds the request header read (slowloris) |
| `ReadTimeout` | 15s | Bounds the whole request read, headers + body |
| `WriteTimeout` | 75s | Bounds handler execution plus response flush |
| `IdleTimeout` | 120s | Reaps idle keep-alive connections |
These are distinct from the 60s per-request handler budget applied by
`chimw.Timeout` in `internal/server/routes.go`, which cancels the request
context but does not touch the socket. `WriteTimeout` is deliberately
larger than that budget: the write deadline is armed once request headers
are read, so a smaller value would sever the connection before a handler
using its full budget could respond. `IdleTimeout` exceeds common
Prometheus scrape intervals so the scraper reuses its connection.
--- ---
## Architecture ## Architecture
+15
View File
@@ -23,6 +23,13 @@ Rationale, Design, TODO, License, Author) if any are still missing.
# Completed Steps # Completed Steps
- 2026-09-21: server timeout test now drives `Run` and asserts the
served `http.Server` carries the timeouts; corrected the inverted
`ReadTimeout` rationale note (#120).
- 2026-09-21: `go mod tidy` dropped the redundant `golang.org/x/sync`
`// indirect` line so `script/bootstrap` leaves a clean tree (#132)
- 2026-08-10: comment-only corrections to `script/bootstrap`, - 2026-08-10: comment-only corrections to `script/bootstrap`,
`script/cibuild`, and `Dockerfile.lint`. The `goimports` pin in `script/cibuild`, and `Dockerfile.lint`. The `goimports` pin in
`script/bootstrap` was justified by a claim that `script/fmt-check` `script/bootstrap` was justified by a claim that `script/fmt-check`
@@ -101,6 +108,14 @@ Rationale, Design, TODO, License, Author) if any are still missing.
so shutdown cannot be extended indefinitely; an `OnStop` context that so shutdown cannot be extended indefinitely; an `OnStop` context that
is already expired on entry with nothing outstanding drains quietly is already expired on entry with nothing outstanding drains quietly
rather than warning about deliveries that were never abandoned rather than warning about deliveries that were never abandoned
- 2026-08-09: `http.Server` now sets all four socket-level timeouts
(`ReadTimeout` 15s, `ReadHeaderTimeout` 10s, `WriteTimeout` 75s,
`IdleTimeout` 120s) as named constants in `internal/server/server.go`,
closing the slowloris / unreaped-keep-alive exposure required by
`REPO_POLICIES.md` before 1.0; `WriteTimeout` is deliberately greater
than the 60s `chimw.Timeout` handler budget so that budget stays
reachable, and tests in `internal/server` pin both the non-zero
values and that relationship (#99)
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs - 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
org-standard v2-schema config used across the org's repos org-standard v2-schema config used across the org's repos
-1
View File
@@ -40,7 +40,6 @@ require (
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.32.0 // indirect golang.org/x/mod v0.32.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.41.0 // indirect golang.org/x/tools v0.41.0 // indirect
+24
View File
@@ -0,0 +1,24 @@
package server
import (
"net/http"
"time"
)
// RequestTimeout exports the handler execution budget applied by
// chimw.Timeout in SetupRoutes, so tests can assert the relationship
// between it and the server's WriteTimeout.
const RequestTimeout time.Duration = requestTimeout
// SetListenPort overrides the port Run binds. A test uses it to hand
// Run an unbindable port so ListenAndServe fails immediately and Run
// returns after storing its http.Server.
func SetListenPort(s *Server, port int) {
s.port = port
}
// HTTPServerOf returns the http.Server that Run built and stored, so a
// test can inspect the timeouts the running server actually carries.
func HTTPServerOf(s *Server) *http.Server {
return s.httpServer
}
+64 -7
View File
@@ -33,8 +33,52 @@ type Params struct {
// shutdownTimeout is how long to wait for graceful shutdown. // shutdownTimeout is how long to wait for graceful shutdown.
const shutdownTimeout = 30 * time.Second const shutdownTimeout = 30 * time.Second
// readHeaderTimeout is the max duration for reading request headers. // Socket-level timeouts for the HTTP server.
const readHeaderTimeout = 10 * time.Second //
// These bound time spent on the connection itself and are a distinct
// control from the per-request handler budget enforced by
// chimw.Timeout(requestTimeout) in routes.go: that one cancels the
// request context after requestTimeout but never touches the socket,
// so without the values below a peer can hold a connection open
// forever (slowloris, unreaped keep-alives).
//
// The one hard constraint between the two controls is
// writeTimeout > requestTimeout. net/http arms the write deadline
// once the request headers have been read, so on a plaintext
// connection it covers handler execution AND the response flush. If
// writeTimeout were <= requestTimeout the server would sever the
// connection before a handler that legitimately consumed its full
// budget could emit anything, making the 60s budget unreachable in
// practice. The margin between them is the response-flush allowance.
//
// The only clients of this service are browsers loading the dashboard
// and a Prometheus scraper; the values are sized for those.
const (
// readHeaderTimeout is the max duration for reading request
// headers.
readHeaderTimeout = 10 * time.Second
// readTimeout bounds reading the entire request, headers plus
// body. Every route here is a GET with no body, so this only
// ever needs to cover headers; the extra 5s over
// readHeaderTimeout is slack, not a real allowance, and keeps a
// body dribbled one byte at a time from holding the read side
// open indefinitely.
readTimeout = 15 * time.Second
// writeTimeout must exceed the requestTimeout handler budget
// (60s) per the note above. The 15s difference is the allowance
// for flushing a completed response to a slow client.
writeTimeout = 75 * time.Second
// idleTimeout reaps keep-alive connections between requests. It
// is deliberately longer than the common Prometheus scrape
// intervals (15s/30s/60s) so the scraper reuses its connection
// rather than reconnecting every cycle, while a browser tab
// left open on the dashboard stops occupying a connection
// within two minutes of going quiet.
idleTimeout = 120 * time.Second
)
// Server is the HTTP server. // Server is the HTTP server.
type Server struct { type Server struct {
@@ -76,16 +120,29 @@ func New(
return srv, nil return srv, nil
} }
// newHTTPServer builds the listening http.Server with every
// socket-level timeout set. All four are set deliberately: a zero
// value in net/http means "no limit", not "some default".
func newHTTPServer(
listenAddr string,
handler http.Handler,
) *http.Server {
return &http.Server{
Addr: listenAddr,
Handler: handler,
ReadTimeout: readTimeout,
ReadHeaderTimeout: readHeaderTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
}
// Run starts the HTTP server. // Run starts the HTTP server.
func (s *Server) Run() { func (s *Server) Run() {
s.SetupRoutes() s.SetupRoutes()
listenAddr := fmt.Sprintf(":%d", s.port) listenAddr := fmt.Sprintf(":%d", s.port)
s.httpServer = &http.Server{ s.httpServer = newHTTPServer(listenAddr, s)
Addr: listenAddr,
Handler: s,
ReadHeaderTimeout: readHeaderTimeout,
}
s.log.Info("http server starting", "addr", listenAddr) s.log.Info("http server starting", "addr", listenAddr)
+136
View File
@@ -0,0 +1,136 @@
package server_test
import (
"testing"
"github.com/spf13/viper"
"go.uber.org/fx"
"sneak.berlin/go/dnswatcher/internal/config"
"sneak.berlin/go/dnswatcher/internal/globals"
"sneak.berlin/go/dnswatcher/internal/handlers"
"sneak.berlin/go/dnswatcher/internal/healthcheck"
"sneak.berlin/go/dnswatcher/internal/logger"
"sneak.berlin/go/dnswatcher/internal/middleware"
"sneak.berlin/go/dnswatcher/internal/notify"
"sneak.berlin/go/dnswatcher/internal/server"
"sneak.berlin/go/dnswatcher/internal/state"
)
// buildServer wires a *server.Server exactly as cmd/dnswatcher does,
// minus the watcher/resolver subtree that would touch live DNS. fx
// builds the object graph but the lifecycle is never started, so no
// OnStart hook runs and nothing listens or resolves. The caller must
// first configure viper (config.New reads it), which is also why the
// caller cannot run in parallel.
func buildServer(t *testing.T) *server.Server {
t.Helper()
var srv *server.Server
app := fx.New(
fx.NopLogger,
fx.Provide(
globals.New,
logger.New,
config.New,
state.New,
healthcheck.New,
notify.New,
middleware.New,
handlers.New,
server.New,
),
fx.Populate(&srv),
)
err := app.Err()
if err != nil {
t.Fatalf("building server graph: %v", err)
}
return srv
}
// TestRunWiresSocketTimeouts pins that the http.Server the running
// server actually serves — the one Run builds and hands to
// ListenAndServe — carries every socket-level timeout, plus the two
// relationships the values must satisfy. Earlier tests asserted these
// on newHTTPServer directly, which left the call site unguarded: a Run
// that built its http.Server inline would drop every timeout with the
// suite still green (https://git.eeqj.de/sneak/dnswatcher/issues/120).
//
// Run is driven to completion with an unbindable port: it builds and
// stores s.httpServer, then ListenAndServe fails at once and Run
// returns without ever listening. The assertions run in the same
// goroutine after Run returns, so reading s.httpServer is free of any
// data race. Nothing here measures elapsed time.
//
// On the ReadTimeout >= ReadHeaderTimeout relationship: a smaller
// ReadTimeout does NOT make the header phase unreachable. net/http's
// (*Server).readHeaderTimeout applies ReadHeaderTimeout directly, so
// the header read keeps its full budget. What breaks is the
// whole-request deadline: once the headers are read, readRequest
// installs a read deadline of t0+ReadTimeout, which is already in the
// past when ReadTimeout is the smaller value, severing the request.
// Verified against the pinned go1.25 net/http (Dockerfile golang
// 1.25-alpine; go.mod go 1.25.5): src/net/http/server.go readRequest
// and (*Server).readHeaderTimeout.
func TestRunWiresSocketTimeouts(t *testing.T) {
// Sets an env var and touches viper global state, so like the
// config tests it cannot use t.Parallel.
viper.Reset()
t.Setenv("DNSWATCHER_TARGETS", "example.com")
srv := buildServer(t)
server.SetListenPort(srv, -1)
srv.Run()
hs := server.HTTPServerOf(srv)
if hs == nil {
t.Fatal("Run did not build an http.Server")
}
if hs.ReadTimeout <= 0 {
t.Errorf("ReadTimeout must be non-zero, got %v", hs.ReadTimeout)
}
if hs.ReadHeaderTimeout <= 0 {
t.Errorf(
"ReadHeaderTimeout must be non-zero, got %v",
hs.ReadHeaderTimeout,
)
}
if hs.WriteTimeout <= 0 {
t.Errorf("WriteTimeout must be non-zero, got %v", hs.WriteTimeout)
}
if hs.IdleTimeout <= 0 {
t.Errorf("IdleTimeout must be non-zero, got %v", hs.IdleTimeout)
}
if hs.WriteTimeout <= server.RequestTimeout {
t.Errorf(
"WriteTimeout (%v) must exceed handler budget (%v)",
hs.WriteTimeout,
server.RequestTimeout,
)
}
if hs.ReadTimeout < hs.ReadHeaderTimeout {
t.Errorf(
"ReadTimeout (%v) must be >= ReadHeaderTimeout (%v)",
hs.ReadTimeout,
hs.ReadHeaderTimeout,
)
}
if hs.Handler != srv {
t.Errorf(
"Run wired handler %T, want the *server.Server",
hs.Handler,
)
}
}