rest

package
v0.0.0-...-cf069df Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2026 License: GPL-3.0 Imports: 32 Imported by: 0

Documentation

Overview

Package rest is the stdlib net/http stack that replaced the gRPC server + grpc-gateway pair (PRD #112, Phase 3); the #134 cutover removed those and made this the single public production listener. The single public listener serves every route through New/routes.

Most of this package is HTTP plumbing the gateway used to provide for free: gzip (gzip.go), query binding (query.go), the error-code→HTTP-status mapping (write.go), clean-path redirects (redirect.go), client-IP resolution (clientip.go), Cache-Control grouping (cachecontrol.go) and Sentry (sentry.go, sentry_boot.go). The request-handling business logic is the per-resource files — milpacs.go, rosters.go, positions.go, awol.go, tickets.go — and that is where new work usually lands.

Middleware chain (PRD order — assembled in chain, New's composition)

sentry → metrics → auth (→ sentryLabel) → gzip → clean-path 307 → mux

Extension points, outermost first:

  • sentryMiddleware (#132, sentry.go): panic recovery at the front of the chain — catches the metrics layer's re-panic, reports the event, and writes the contract 500; 5xx Sentry reports hook the writeError choke point. Env-gated by SENTRY_DSN (rest.SetupSentry): without a client it is a complete no-op. Its route/key-id tags reach this OUTER layer via the sentryLabels context holder, filled by the sentryLabel wrapper inside auth (the same mechanism as metricLabels below).
  • metricsMiddleware (#130, metrics.go): Prometheus request counter (route/method/status/key_id) and latency histogram (route/method). Outside auth, so rejected requests are counted. The route and key_id labels reach this OUTER layer via the context label-holder (metricLabels): AuthMiddleware fills the key-id slot, the routeLabel wrapper inside the mux fills the route slot from r.Pattern. The exposition is never served through this chain; MetricsHandler is served on its own INTERNAL-ONLY listener (:9090, servers/server.go).
  • AuthMiddleware: bearer-key validation with the golden-pinned two-tier plain-text 401s. Runs BEFORE routing, so an unknown path without credentials is a 401, not a 404 (golden-pinned). Scope checks are per-route, not here — see requireScope.
  • GzipMiddleware: response compression. Inside auth (401s are never gzipped), outside the mux (every routed response, including the JSON 404, compresses).
  • cleanPathRedirect (redirect.go): the mux's clean-path 307 answered in front of the mux with the contract JSON body and the bounded catch-all metering label (ruled, #128 round 3 — enumerated deliberate break). Inside gzip, so the redirect body compresses like every routed response; clean paths pass through untouched.
  • mux: the Go 1.22+ pattern-routing http.ServeMux.

Adding a route (the fan-out recipe, #126–#129)

  1. Wire types in types/ (follow the conventions in the package doc).
  2. Handler in this package: map the datastore result to the wire types (allocate empty collections!), writeJSON on success, writeError with the frozen message string on failure. Query parameters go through newQueryBinder: bind EVERY field first, then check b.Err() exactly once and 400 its text verbatim — a forgotten Err() check silently drops a frozen 400. Routes whose old generated handler called req.ParseForm() parse strictly via bindListQuery instead of r.URL.Query().
  3. Register in routes(): handle(mux, "GET /api/v1/...", "<scope>", <max-age>, handler) — the scope gate and the route group's Cache-Control max-age (#131, cachecontrol.go) are required arguments, not wrapping conventions. Path parameters via r.PathValue. Wrong-method and unknown paths are already covered by the mux fallback (405+Allow / JSON 404).
  4. Spec operation block in openapi/openapi.yaml (CI-enforced two-way coverage, contract/spec_test.go) — its 200 response must declare the Cache-Control const matching the registered max-age (structural guard in contract/spec_test.go, observed-equals-declared in rest/spec_test.go).
  5. Goldens green: add the route's battery case names to implementedCases in rest_test.go — the replay harness does the rest.
  6. Classify every new case's request path in specRoutes (rest/spec_test.go) so the new-stack spec validation covers the route's observed responses — an unclassified path fails that suite, it never skips.

Index

Constants

This section is empty.

Variables

View Source
var (
	Info  = log.New(os.Stdout, "INFO: ", log.LstdFlags)
	Warn  = log.New(os.Stdout, "WARNING: ", log.LstdFlags)
	Error = log.New(os.Stdout, "ERROR: ", log.LstdFlags)
)

Functions

func AuthMiddleware

func AuthMiddleware(ds datastores.Datastore, next http.Handler) http.Handler

AuthMiddleware authenticates every request against the upstream key tables (ADR 0004) and attaches the validated key to the request context. It runs BEFORE routing — an unknown path without credentials is a 401, not a 404 (golden-pinned). Authorization (scope membership) is per-route: see requireScope.

The public middleware constructor the chain composition uses (rest.New).

func ContextWithKey

func ContextWithKey(ctx context.Context, key *datastores.ApiKeyResult) context.Context

ContextWithKey returns a new context carrying the given API key result.

func DocsHandler

func DocsHandler(version string) http.Handler

DocsHandler serves the embedded Scalar docs UI and the OpenAPI 3.1 spec at the same URLs the old gateway used (everything outside /api). The spec has its info.version stamped from version; all other assets (the Scalar shell, its vendored bundle, and the theme) are served verbatim from the embedded filesystem.

version is the build-time var (servers.version via -ldflags, "dev" locally), threaded in by the composition root.

func FlushSentryOnShutdown

func FlushSentryOnShutdown()

FlushSentryOnShutdown installs a signal handler that flushes buffered Sentry events before the process exits. The stack has no graceful shutdown path (Start blocks on Serve and the process dies by signal); this is the minimal hook so error events from the final moments are not lost. The composition root calls it only when SetupSentry returned true, so the no-DSN path keeps the default signal behavior exactly — guarded here as well as at the call site, because installing the handler without a client would silently rewrite the process's signal semantics for nothing.

func GzipMiddleware

func GzipMiddleware(next http.Handler) http.Handler

GzipMiddleware is the compression layer. It sits inside auth (401s are never gzipped) and outside the mux, so every routed response — including the JSON 404 — compresses when the client asks.

A handler-sent 1xx on a gzip-negotiated request carries Content-Encoding: gzip in the interim response — the stdlib sends the live header map per RFC 8297 — pre-existing, adjacent to the stale-Content-Length holes #175 closed.

func InitTrustedProxies

func InitTrustedProxies() error

InitTrustedProxies reads/parses/caches TRUSTED_PROXIES once at startup. Returns an error on a non-empty-but-malformed value (the caller makes it fatal). Empty/unset trusts nothing.

func KeyFromContext

func KeyFromContext(ctx context.Context) *datastores.ApiKeyResult

KeyFromContext returns the *ApiKeyResult attached to ctx, or nil if none.

func MetricsHandler

func MetricsHandler() http.Handler

MetricsHandler serves the Prometheus exposition for this process. It must be mounted on the INTERNAL-ONLY listener, never in the public chain — per-key traffic counts are operational data.

Deploy-config assertion (for the cutover slice #134, which mounts this on its own listener): the metrics port is internal-only, meaning

  1. docker-compose must NOT publish it (no entry under the api service's `ports:`), and
  2. the reverse proxy must NOT route it (no location/upstream forwarding to the metrics port).

Verify at cutover, from outside the host:

curl https://<public-host>/metrics        → 404 (falls through to the docs
                                            file server; the metrics handler
                                            is not mounted on the public
                                            listener)
curl http://<public-host>:<metrics-port>/ → connection refused/timeout
                                            (port unpublished + unrouted)

and from inside the host network: curl localhost:<metrics-port>/metrics serves this exposition.

func New

New assembles the new stack: the route mux wrapped in the PRD middleware chain (sentry → metrics → auth (→ sentryLabel) → gzip → clean-path 307 → mux; the one definition lives on chain below). The returned handler serves the /api surface; non-API paths (the docs UI) are served by rest.DocsHandler on the same listener (composed in servers.servPublic).

rc is the tickets reference cache (status/priority/prefix names, the category tree) the tickets datastore methods consume — at cutover (#134) the caller passes the refreshed referencecache.Cache the old stack already maintains (servers.Start wires it today). It must be WARMED (Refresh run and the poller keeping it fresh), not merely non-nil: a cold cache degrades silently — empty categories, blank status/priority/prefix names, and category filters collapsing from subtree to exact-match. New refuses nil outright: a nil cache is a guaranteed panic on the first tickets request against the real datastore (recovery exists only when SENTRY_DSN is set — and a panic-per-request service is broken either way).

func SetupSentry

func SetupSentry(release string) bool

SetupSentry initialises Sentry error capture (errors only, no tracing) when SENTRY_DSN is present in the environment — the same gate as Phase 0's servers.setupSentry, which this replaces at cutover (#134). Without a DSN nothing is initialised — no client, no capture, local/dev unaffected; the only side effect is one Info line making the disabled state visible at boot. Returns whether capture was enabled.

release is the build-time version (the -X servers.version injection today; the cutover slice passes it through) — it stamps every event's Release tag.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL