http

package
v0.0.0-...-2b68f72 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Overview

Package http is the HTTP adapter — the chi-based router that fronts the application use cases. It owns three responsibilities:

  1. Compose the fixed middleware chain (recover → correlation ID → request log → metrics → inbound rate limit → idempotency → handler) in the order mandated by CLAUDE.md §3 / §10.
  2. Mount the HTTP handlers that translate REST calls into use case invocations and translate domain errors into RFC 7807 problem responses.
  3. Mount auxiliary endpoints — health, /metrics, Swagger UI, the WebSocket upgrade endpoint.

The package name collides with stdlib net/http; callers import this package aliased as httpadapter and the stdlib as nethttp when both are needed in the same file. Inside this package the stdlib is aliased; chi handlers themselves are stdlib-compatible.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotImplemented = errors.New("not implemented")

ErrNotImplemented is the sentinel an operation returns when no concrete handler has been wired to it yet. The strict-server error handler maps it to a 501 RFC 7807 problem response. Phase 4 tasks override one operation each in the Server struct; until then the embedded unimplementedServer returns this error.

Functions

func ContextWithCorrelationID

func ContextWithCorrelationID(parent context.Context, id string) context.Context

ContextWithCorrelationID is the matching re-export of correlation.WithContext.

func CorrelationIDFromContext

func CorrelationIDFromContext(ctx context.Context) string

CorrelationIDFromContext is a thin re-export of correlation.FromContext kept for backward compatibility with existing call sites in this adapter. New code should reach for the infrastructure package directly.

func CorrelationIDMiddleware

func CorrelationIDMiddleware(gen ports.IDGenerator) func(nethttp.Handler) nethttp.Handler

CorrelationIDMiddleware reads X-Correlation-ID from the inbound request, generates a new id via gen when the header is missing or blank, stashes the resolved id in the request context, and echoes it in the response header. Downstream handlers retrieve it via CorrelationIDFromContext.

The middleware deliberately takes the full ports.IDGenerator rather than a narrower factory function — the same generator is wired into the application use cases (CreateNotification, ProcessNotification, ...) so the http adapter shares it instead of constructing a duplicate.

func IdempotencyMiddleware

func IdempotencyMiddleware(store ports.IdempotencyStore) func(nethttp.Handler) nethttp.Handler

IdempotencyMiddleware caches the response of write requests keyed by the client-supplied Idempotency-Key header so a duplicate request returns the original response without re-running the handler (CLAUDE.md §3.9).

Semantics:

  • No header → pass through; the store is never touched.
  • Header + cache hit → return cached body and Content-Type with status 200 (intentionally collapses 201/202 to 200 so the client can distinguish "you saw this before" from "we just created this").
  • Header + cache miss → run the handler with a capturing writer. 2xx responses are cached with a 24h ttl; non-2xx are not cached because the result may be transient (5xx) or the client may want to retry with corrections (4xx).
  • Store Get error → fail-open (run the handler). Availability beats strict enforcement when Redis hiccups.
  • Store Set error → log and continue; the client still gets the handler's response. The cache will be re-populated on the next attempt.

func InboundRateLimitMiddleware

func InboundRateLimitMiddleware(limiter ports.RateLimiter) func(nethttp.Handler) nethttp.Handler

InboundRateLimitMiddleware throttles inbound HTTP traffic at a fixed budget of 60 requests/minute per client IP (CLAUDE.md §2.6 / §10). The middleware delegates the counting itself to ports.RateLimiter so production wires the Redis-backed implementation while tests inject a deterministic fake.

Bucket key shape: "ip:<addr>". The "ip:" prefix is the inbound namespace separator — outbound (per channel) uses "channel:<name>" against the same RateLimiter and must not collide (CLAUDE.md §2.6).

Failure policy: when the limiter returns an error (Redis unreachable etc.) the middleware logs and lets the request through (fail-open). Availability is more valuable than strict enforcement at the inbound edge — a brief abuse window beats taking the whole API offline when Redis hiccups.

func MetricsMiddleware

func MetricsMiddleware(m *metrics.Metrics) func(nethttp.Handler) nethttp.Handler

MetricsMiddleware records one http_requests_total increment and one http_request_duration_seconds observation per request (CLAUDE.md §12.1). The label set uses the chi route PATTERN (not URL.Path) so high-cardinality path params like /notifications/{id} collapse into a single series.

Unmatched requests fall into a fixed "unknown" pattern so a script spraying random paths cannot blow up the cardinality.

func MountDocs

func MountDocs(r chi.Router)

MountDocs registers the documentation handlers on the supplied router:

  • GET /docs and /docs/ → Swagger UI HTML shell.
  • GET /docs/openapi.yaml → the embedded spec as text/yaml.

Mounted outside the strict-server because OpenAPI 3.0 does not describe its own documentation surface — the page is a static asset in spirit, even though one byte of it is the spec embedded at build.

func NewRouter

func NewRouter(cfg Config) *chi.Mux

NewRouter returns a chi.Mux with Recoverer wired first and the configured middlewares appended in order. Callers register routes on the returned mux via Get/Post/Mount/Route — chi's behavior is unchanged, this constructor only fixes the chain order.

Recoverer is always present so a panic in any handler degrades to a 500 instead of taking down the API process — the worker fleet and reconciler continue running.

func RespondWithError

func RespondWithError(w nethttp.ResponseWriter, r *nethttp.Request, err error)

RespondWithError is the unified strict-server error handler — wire it into BOTH StrictHTTPServerOptions.RequestErrorHandlerFunc (body decode failures) and ResponseErrorHandlerFunc (handler errors). It bridges the strict-server world to the project's RFC 7807 translator (problem.go) so every operation gets uniform error semantics — adding a new domain error never requires touching this function, only the errorMappings table in problem.go.

Special cases handled inline rather than via errorMappings:

  • ErrNotImplemented → 501. Purely an http-adapter concern (the domain has no concept of "operation not wired yet"); keeping it here preserves the layering.
  • JSON decode errors (*json.SyntaxError, *json.UnmarshalTypeError, io.ErrUnexpectedEOF) → 400. The strict-server wraps these and passes them through RequestErrorHandlerFunc; a 400 with a generic Detail is the correct semantic response.

func WriteError

func WriteError(w nethttp.ResponseWriter, r *nethttp.Request, err error)

WriteError is the single translation function CLAUDE.md §3.5 mandates: it maps a domain or port error to the appropriate RFC 7807 problem response. Adding a new domain error means adding one branch to errorMappings — the test catalog in problem_test.go is the spec.

Calling WriteError with a nil error is a deliberate no-op so handlers that defer a single `if err != nil { WriteError(...) }` check don't have to guard the call site too.

func WriteProblem

func WriteProblem(w nethttp.ResponseWriter, r *nethttp.Request, p Problem)

WriteProblem serializes p as application/problem+json and writes it to w with the status carried in p. The request is used to fill in fields the caller didn't set explicitly: Instance defaults to the request URL path, CorrelationID defaults to the value in r.Context() (zero when the middleware has not run).

Type defaults to "about:blank" per RFC 7807 §3.1 — a Problem without a more specific type still validates against the spec.

Types

type CancelNotificationExecutor

type CancelNotificationExecutor func(ctx context.Context, in application.CancelNotificationInput) (*domain.Notification, error)

CancelNotificationExecutor is the slim contract for PATCH /api/v1/notifications/{id}/cancel. Production wires (*application.CancelNotification).Execute; tests pass a closure.

type Config

type Config struct {
	// Middlewares is the ordered list applied after Recoverer. Each
	// entry has the standard func(http.Handler) http.Handler shape so
	// it can come from chi/middleware, this package, or a third-party.
	Middlewares []func(nethttp.Handler) nethttp.Handler
}

Config wires the optional middlewares the chain composes after the always-on Recoverer. The Middlewares slice is applied in order, so the caller (cmd/api) is responsible for preserving the canonical chain:

recover (always first, wired by NewRouter)
  → correlation ID
  → request log
  → metrics
  → inbound rate limit
  → idempotency
  → handler

nil entries are silently skipped — tests and reduced-stack composes pass a partial chain without panicking. Tasks 2-4 in phase 4 add the concrete middleware constructors that fill the slots.

type CreateBatchExecutor

type CreateBatchExecutor func(ctx context.Context, in application.CreateBatchInput) (*domain.Batch, error)

CreateBatchExecutor is the slim contract for POST /api/v1/notifications/batch. Production wires (*application.CreateBatch).Execute; tests pass a closure.

type CreateNotificationExecutor

type CreateNotificationExecutor func(ctx context.Context, in application.CreateNotificationInput) (*domain.Notification, error)

CreateNotificationExecutor is the slim function-type contract the CreateNotification handler depends on. Mirrors the pattern used by the asynq processor: the production wiring (cmd/api) passes (*application.CreateNotification).Execute, tests pass a closure that records inputs and returns a controlled outcome.

type CreateTemplateExecutor

type CreateTemplateExecutor func(ctx context.Context, in application.CreateTemplateInput) (*domain.Template, error)

CreateTemplateExecutor is the slim contract for POST /api/v1/templates.

type DeleteTemplateExecutor

type DeleteTemplateExecutor func(ctx context.Context, in application.DeleteTemplateInput) error

DeleteTemplateExecutor is the slim contract for DELETE /api/v1/templates/{id}.

type GetBatchExecutor

type GetBatchExecutor func(ctx context.Context, in application.GetBatchInput) (*domain.Batch, error)

GetBatchExecutor is the slim contract for GET /api/v1/notifications/batch/{id}. Production wires (*application.GetBatch).Execute; tests pass a closure.

type GetNotificationExecutor

type GetNotificationExecutor func(ctx context.Context, in application.GetNotificationInput) (*domain.Notification, error)

GetNotificationExecutor is the slim contract for GET /api/v1/notifications/{id}. Production wires (*application.GetNotification).Execute; tests pass a closure.

type GetNotificationTraceExecutor

type GetNotificationTraceExecutor func(ctx context.Context, in application.GetNotificationTraceInput) ([]*domain.NotificationLog, error)

GetNotificationTraceExecutor is the slim contract for GET /api/v1/notifications/{id}/trace. Production wires (*application.GetNotificationTrace).Execute; tests pass a closure.

type GetTemplateExecutor

type GetTemplateExecutor func(ctx context.Context, in application.GetTemplateInput) (*domain.Template, error)

GetTemplateExecutor is the slim contract for GET /api/v1/templates/{id}.

type JSONMetricsProvider

type JSONMetricsProvider func(ctx context.Context) (JSONMetricsSnapshot, error)

JSONMetricsProvider returns one snapshot per scrape. Production wires a Prometheus-querying provider (or an inline counter aggregation); tests inject a closure.

type JSONMetricsSnapshot

type JSONMetricsSnapshot struct {
	CreatedPerMinute   int
	DeliveredPerMinute int
	FailedPerMinute    int
	QueueDepth         int
	SuccessRate        *float64
}

JSONMetricsSnapshot is the small, JSON-friendly subset of the Prometheus exposition data that /api/v1/metrics returns. Used by clients that cannot consume Prometheus text format (dashboards, ad-hoc scripts). SuccessRate is a pointer so the provider can explicitly signal "no data this window" instead of an ambiguous 0.

type ListNotificationsExecutor

ListNotificationsExecutor is the slim contract for GET /api/v1/notifications. Production wires (*application.ListNotifications).Execute; tests pass a closure.

type ListTemplatesExecutor

type ListTemplatesExecutor func(ctx context.Context, in application.ListTemplatesInput) (application.ListTemplatesOutput, error)

ListTemplatesExecutor is the slim contract for GET /api/v1/templates.

type Problem

type Problem struct {
	Type          string `json:"type"`
	Title         string `json:"title"`
	Status        int    `json:"status"`
	Detail        string `json:"detail,omitempty"`
	Instance      string `json:"instance,omitempty"`
	CorrelationID string `json:"correlation_id,omitempty"`
}

Problem is the on-wire shape of an RFC 7807 Problem Details object (CLAUDE.md §3.5). Title and Status are required; Type defaults to "about:blank" per RFC 7807 §3.1 when omitted; Detail and Instance are optional. CorrelationID is a non-standard but commonly-added member (CLAUDE.md §10 example).

type ReadinessCheck

type ReadinessCheck func(ctx context.Context) error

ReadinessCheck verifies that one downstream dependency is reachable. /healthz/ready invokes every configured check in turn; any error flips the response to 503. Production wires one check per critical dependency (Postgres ping, Redis ping); tests inject closures.

type ReplaceTemplateExecutor

type ReplaceTemplateExecutor func(ctx context.Context, in application.ReplaceTemplateInput) (*domain.Template, error)

ReplaceTemplateExecutor is the slim contract for PUT /api/v1/templates/{id}.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is the adapter that implements api.StrictServerInterface by dispatching each operation to its application use case. Every handler method begins with a nil-check on its executor and returns ErrNotImplemented (→ 501) when absent — tests wire only the slots they exercise.

Concurrency: Server holds only constructor-injected references; it is safe to share across goroutines once NewServer returns.

func NewServer

func NewServer(opts ServerOptions) *Server

NewServer wires the executors carried by opts into a Server. Any operation whose executor is nil falls through to a 501 response — the nil-check lives in each handler method.

func (*Server) CancelNotification

CancelNotification overrides the embedded stub and dispatches to the wired executor. It is the operation behind `PATCH /api/v1/notifications/{id}/cancel`.

Notable error semantics handled entirely by RespondWithError:

  • ports.ErrNotFound → 404 (id does not exist).
  • *domain.TransitionError → 409 (already delivered/failed/cancelled). The detail string carries the offending from→to pair so support can immediately tell what state the notification was already in.

func (*Server) CreateBatch

CreateBatch overrides the embedded unimplementedServer stub and dispatches to the wired CreateBatch executor. It is the operation behind `POST /api/v1/notifications/batch`.

The response intentionally omits the member notifications — the openapi.yaml description spells out that POST 202 keeps the payload small and the caller fetches members later via `GET /api/v1/notifications/batch/{id}`. Returning hundreds of full Notification structs on every create would dwarf the actual signal (batch id and size) the client needs in the synchronous reply.

func (*Server) CreateNotification

CreateNotification overrides the embedded unimplementedServer stub and dispatches to the wired executor. It is the operation behind `POST /api/v1/notifications`.

Flow:

  1. Reject an absent body — the strict-server already enforces Content-Type=application/json and JSON-decodes it, but the body pointer is nil when the caller sends an empty request. Treat that as a domain.ValidationError so the error handler emits a 400 problem.
  2. Map the wire-level request and params into the use case input and invoke the executor. Validation errors from the domain (ErrInvalidChannel, etc.) propagate as-is; the error handler dispatches them via WriteError.
  3. On success, convert the domain.Notification to its wire shape, compute the Location URI, and return a typed 202 response.

If the embedded executor is nil (no override wired) we still need a usable signal — return ErrNotImplemented so RespondWithError emits a 501 just like the unimplementedServer stub does for the other operations.

func (*Server) CreateTemplate

CreateTemplate creates a new template and returns 201 with the new resource and a Location header.

func (*Server) DeleteTemplate

DeleteTemplate removes a template. Returns 204 on success, 404 when the id is unknown.

func (*Server) GetBatch

GetBatch overrides the embedded stub and dispatches to the wired executor. It is the operation behind `GET /api/v1/notifications/batch/{id}`.

In contrast to POST 202 (which omits the member notifications to keep the synchronous reply small), GET inlines them — the client has explicitly asked for the full picture.

func (*Server) GetHealthzLive

GetHealthzLive is the liveness probe. It always returns 200 — its only job is to confirm the process is running and the HTTP server is accepting requests. Kubernetes uses this to decide whether to restart the pod; including downstream checks would cause a Redis hiccup to restart every API replica simultaneously.

func (*Server) GetHealthzReady

GetHealthzReady runs every configured ReadinessCheck and reports ready only when all of them succeed. A single failure flips the response to 503 — Kubernetes stops routing traffic but the pod stays alive so the dependency can recover.

The check loop returns on the first failure (no need to keep probing — the verdict is already 503). The failed check's error is logged with structured fields so operators can correlate the 503 with the offending dependency.

func (*Server) GetJSONMetrics

GetJSONMetrics renders /api/v1/metrics. The endpoint exists for clients that cannot speak the Prometheus text exposition format (dashboards, ad-hoc scripts, smoke tests) — the data is the same data /metrics exposes, just JSON-shaped and trimmed to the most useful counters.

SuccessRate is intentionally a pointer in the snapshot: explicit `nil` means "no traffic in the window, no rate to report" so the API does not lie about the success of zero deliveries.

func (*Server) GetNotification

GetNotification overrides the embedded unimplementedServer stub and dispatches to the wired GetNotification executor. It is the operation behind `GET /api/v1/notifications/{id}`.

ports.ErrNotFound returned by the use case flows through RespondWithError and is translated to a 404 problem — no special handling here. The handler only ferries the parsed path id into the use case and the resulting domain.Notification back out.

func (*Server) GetNotificationTrace

GetNotificationTrace overrides the embedded stub and dispatches to the wired executor. It is the operation behind `GET /api/v1/notifications/{id}/trace`.

The use case verifies the notification exists (so an unknown id produces 404 instead of an empty 200) — no special handling here. The handler maps each domain.NotificationLog into its wire shape and packages them under the notification_id top-level field.

func (*Server) GetPrometheusMetrics

GetPrometheusMetrics overrides the embedded stub and renders the registered metric set in the Prometheus exposition format. The handler is wired through the strict-server (not promhttp.Handler mounted on a sibling route) so the same chain — request id, log, metrics middleware — wraps it as every other endpoint.

Encoding cost: every scrape rebuilds the response body. Production scrape intervals are 10-60s; the gather + encode for a few hundred metrics is dominated by network IO, so the simplicity is the right trade.

func (*Server) GetTemplate

GetTemplate returns a single template by id, or a 404 problem when the id is unknown.

func (*Server) ListNotifications

ListNotifications overrides the embedded stub and dispatches to the wired executor. It is the operation behind `GET /api/v1/notifications`.

The use case owns:

  • String parsing of status and channel (an invalid value surfaces domain.ErrInvalidStatus / ErrInvalidChannel which the translator turns into a 400).
  • Limit clamping (defaults and ceilings are policy, not transport).

The handler only ferries query params into ListNotificationsInput and the resulting page out into the wire-level NotificationPage.

func (*Server) ListTemplates

ListTemplates returns a page of templates. The use case currently applies only the limit filter; cursor and channel filters are forward-compatible (the use case ignores them and the response omits next_cursor).

func (*Server) ReplaceTemplate

ReplaceTemplate updates a template's fields. PUT semantics: every field in the request replaces the existing value; CreatedAt is preserved by the use case.

type ServerOptions

type ServerOptions struct {
	CreateNotification   CreateNotificationExecutor
	CreateBatch          CreateBatchExecutor
	GetNotification      GetNotificationExecutor
	ListNotifications    ListNotificationsExecutor
	CancelNotification   CancelNotificationExecutor
	GetNotificationTrace GetNotificationTraceExecutor
	GetBatch             GetBatchExecutor

	CreateTemplate  CreateTemplateExecutor
	GetTemplate     GetTemplateExecutor
	ListTemplates   ListTemplatesExecutor
	ReplaceTemplate ReplaceTemplateExecutor
	DeleteTemplate  DeleteTemplateExecutor

	// ReadinessChecks runs against /healthz/ready. Empty means the
	// API has no downstream dependencies to verify and reports ready
	// the moment it can accept HTTP.
	ReadinessChecks []ReadinessCheck

	// PrometheusGatherer is the source for /metrics. Production wires
	// prometheus.DefaultGatherer (or a custom registry); leave nil and
	// the endpoint falls through to 501 via the embedded stub.
	PrometheusGatherer prometheus.Gatherer

	// JSONMetrics returns the snapshot rendered by /api/v1/metrics.
	JSONMetrics JSONMetricsProvider
}

ServerOptions bundles the per-operation executors the Server needs. Each operation has its own slot so partial wiring is legal — every handler method on Server begins with a nil-check on its executor and returns ErrNotImplemented (→ 501) when absent. Production composes the full set; tests inject only what they exercise.

type WebSocketHandler

type WebSocketHandler struct {
	// contains filtered or unexported fields
}

WebSocketHandler upgrades incoming HTTP requests to WebSocket connections and runs the subscribe/unsubscribe protocol against the shared Hub. Production wiring (cmd/api) mounts this handler at `GET /api/v1/ws/notifications`; the openapi spec deliberately does NOT model this endpoint because OpenAPI 3.0 has no native WebSocket description.

func NewWebSocketHandler

func NewWebSocketHandler(hub *wsadapter.Hub) *WebSocketHandler

NewWebSocketHandler wires the handler to the shared Hub.

func (*WebSocketHandler) ServeHTTP

ServeHTTP performs the upgrade and runs the client's read loop until the connection closes. UnsubscribeAll is deferred so the Hub never leaks references to dead connections.

AcceptOptions: OriginPatterns="*" — CORS validation belongs in a dedicated middleware layer at the edge, not in the WebSocket handshake itself (mixing the two has bitten other projects).

Directories

Path Synopsis
Package api provides primitives to interact with the openapi HTTP API.
Package api provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

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