Documentation
¶
Overview ¶
Package middleware provides HTTP middleware for the emulator.
CORS adds Cross-Origin Resource Sharing headers so that browser-based AWS SDK clients (e.g. the Overcast web UI) can talk directly to the emulator without going through a backend-for-frontend proxy.
This is deliberately permissive — the emulator is a local dev tool, not a security boundary. All origins, methods, and headers are allowed.
Package middleware contains HTTP middleware functions for the emulator's request pipeline. Each middleware is a standard net/http middleware — it takes a handler and returns a handler. This is identical to Express middleware in concept: (req, res, next) => void.
In Go:
func MyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// do something before
next.ServeHTTP(w, r)
// do something after
})
}
Index ¶
- Constants
- func CORS(next http.Handler) http.Handler
- func ContextWithRegion(ctx context.Context, region string) context.Context
- func DrainBody(next http.Handler) http.Handler
- func HostDispatch(rows *[]HostRouteRow) func(http.Handler) http.Handler
- func HostRouteService(host string) (service string, ok bool)
- func IAMEnforce(enabled bool, st state.Store, logger *zap.Logger) func(http.Handler) http.Handler
- func InvalidateIAMEnforceCache()
- func Logger(logger *zap.Logger, clk clock.Clock) func(http.Handler) http.Handler
- func NotReady(store state.Store) func(http.Handler) http.Handler
- func Protocol(identifiers []codec.Identifier) func(http.Handler) http.Handler
- func Recovery(logger *zap.Logger) func(http.Handler) http.Handler
- func Region(next http.Handler) http.Handler
- func RegionFromContext(ctx context.Context, fallback string) string
- func RequestEvents(busPtr **events.Bus, clk clock.Clock) func(http.Handler) http.Handler
- func RequestID(next http.Handler) http.Handler
- func S3VirtualHost(next http.Handler) http.Handler
- func S3VirtualHostFor(hostname string, logger ...*zap.Logger) func(http.Handler) http.Handler
- func ServiceFromCredential(r *http.Request) string
- func SigV4(validate bool, secretResolver SecretResolver, logger *zap.Logger, ...) func(http.Handler) http.Handler
- type HostRouteMatch
- type HostRouteRow
- type SecretResolver
Constants ¶
const DefaultSigV4Secret = defaultSigV4Secret
DefaultSigV4Secret is the fallback signing secret used when no SecretResolver is configured or when the resolver cannot find a secret for the given access key. The value "test" matches the default credentials used by the AWS SDK and CLI in local-dev workflows.
Variables ¶
This section is empty.
Functions ¶
func CORS ¶
CORS returns a middleware that sets permissive CORS headers on every response and handles preflight OPTIONS requests.
func ContextWithRegion ¶
ContextWithRegion returns a child context carrying region, suitable for background goroutines that need to access region-scoped stores outside a request context.
func DrainBody ¶
DrainBody returns a middleware that:
- Drains and closes the request body after the handler returns.
- Buffers the response body so that a Content-Length header is always set before the first byte reaches the client.
Together these prevent the Go AWS SDK v2 warning:
"WARN failed to close HTTP response body, this may affect connection reuse"
The warning fires on the CLIENT side when resp.Body.Close() fails. The root cause is a server response without Content-Length: the client relies on chunked transfer or connection close to detect the end of the body, and any framing issue makes Close() return an error. Setting Content-Length on every response eliminates the problem.
If the handler calls http.Flusher.Flush() (streaming / SSE) or the buffered body exceeds maxResponseBuffer, the middleware switches to direct pass-through so large or streaming responses are never fully buffered.
func HostDispatch ¶
func HostDispatch(rows *[]HostRouteRow) func(http.Handler) http.Handler
HostDispatch returns middleware that recognises Host-routed AWS-style addresses (see ParseHostRoute) and applies the matching row's Rewrite, before chi's router runs. rows is read on every request via the pointer, so callers can declare it early in the middleware chain (chi requires all r.Use calls before any route registration) and populate it later once the owning services exist — the same pattern already used for this router's query-dispatcher and event-bus wiring.
Hosts that don't match any row — including S3's own virtual-hosted forms, handled separately by S3VirtualHostFor — pass through unchanged, which is what lets unknown/foreign hosts fall through to the S3 catch-all per AGENTS.md "Routing fallthrough is S3" instead of 404ing here.
func HostRouteService ¶
HostRouteService reports the detectService() label for a Host header that matches the host-route grammar (see ParseHostRoute) — e.g. "apigateway" for an execute-api Host. detectService (logger.go) calls this so a request's log label always matches what HostDispatch actually routed it to; there is no separate/parallel suffix list to keep in sync.
func IAMEnforce ¶
IAMEnforce enforces opt-in IAM authorization.
func InvalidateIAMEnforceCache ¶
func InvalidateIAMEnforceCache()
func Logger ¶
Logger logs every request with structured fields: real AWS API calls and other requests at INFO, internal health/readiness and /_debug/* polling at TRACE (see isOperationalPollPath). When stdout is a terminal, each line is prefixed with the service badge and (when known) an operation badge so log lines are easy to scan at a glance. Failed requests (5xx) are logged at ERROR level regardless of path.
func NotReady ¶
NotReady rejects a request with a 503 while the storage backend is still completing a one-time startup migration (see internal/state/migrate.go), instead of letting the request observe whatever the store would otherwise do during that window: persistent mode blocks the request indefinitely inside ensureReady, and hybrid mode's TierHot reads silently return "not found" for data that exists once migration finishes, because the post-migration seed hasn't populated memory yet (see state.NotReadyReporter and HybridStore.NotReady for the precise window this covers).
Internal Overcast endpoints (any path starting with "/_" — /_debug, /_health, /_/info, /_overcast/*, ...) are exempt, so operators can still check status, inspect debug state, or poll init-hook progress while a migration is in flight. No real AWS API request path starts with "/_".
store is checked once per request via a non-blocking type assertion to state.NotReadyReporter — stores that don't implement it (MemoryStore, WALStore) are always treated as ready, the same convention state.ReadyAwaiter already uses.
func Protocol ¶
Protocol is the wire-protocol detection middleware. It walks a list of codec.Identifiers in precision order; on the first match it stashes the codec and operation name in the request context (retrievable via codec.FromContext) and forwards the request unchanged.
The middleware NEVER:
- consumes the request body,
- rejects a request, or
- alters the request in any way other than adding context values.
On no match it forwards the request unchanged, so legacy handlers continue to function exactly as before. Rejection of unsupported protocols for opted-in services happens at the dispatcher boundary, not here.
This middleware is always-on as of Phase 6 completion.
func Recovery ¶
Recovery catches any panic from a handler, logs it with a stack trace, and returns a 500 InternalError response. Without this, a panic in one handler would crash the entire server process.
In Go, a "panic" is like an uncaught exception — recover() is the equivalent of a catch-all try/catch block.
func Region ¶
Region extracts the AWS region from each request and stores it in the context. Resolution order (first non-empty wins):
- X-Overcast-Region header (internal override used by the CloudFormation provisioner)
- SigV4 Authorization header Credential scope: AKID/DATE/REGION/SERVICE/aws4_request
- Host header subdomain: <id>.execute-api.<region>.<base> — the canonical AWS API Gateway invoke URL shape (also supported by LocalStack).
If none yield a region the context is left unchanged and handlers fall back to cfg.Region (OVERCAST_DEFAULT_REGION, default "us-east-1"). This mirrors how LocalStack resolves region: always from the request, never from a server-wide setting.
func RegionFromContext ¶
RegionFromContext returns the per-request region stored by the Region middleware. If absent, returns fallback.
func RequestEvents ¶
RequestEvents publishes a request:Received event onto the bus for every incoming HTTP request. The bus is injected via a pointer-to-pointer so it can be set after middleware registration (the bus is created late in router.New). If the bus is nil at request time, publishing is skipped.
This middleware intentionally mirrors the Logger middleware's responseWriter + detectService/detectOperation pattern — both intercept the request lifecycle to capture the same metadata for different purposes (logging vs event publishing).
Performance: each event is enqueued on the bus's 4096-capacity buffered worker pool. Publish returns immediately in the common case; it only waits if all 4096 slots are occupied by in-flight work items and 16 workers haven't caught up yet. When no SSE client is connected there are no wildcard subscribers, so zero work items are enqueued (zero overhead).
func RequestID ¶
RequestID attaches a unique request ID to every request context and response header. All subsequent middleware and handlers retrieve it via protocol.RequestIDFromContext(r.Context()).
func S3VirtualHost ¶
S3VirtualHost detects S3 virtual-hosted-style requests and rewrites the URL path to path-style so chi's /{bucket}/* routes match correctly.
Virtual-hosted-style sends the bucket name in the Host header:
Host: mybucket.localhost:4566 → /key rewritten to /mybucket/key Host: mybucket.s3.localhost:4566 → /key rewritten to /mybucket/key Host: mybucket.s3.us-east-1.localhost → /key rewritten to /mybucket/key
Path-style requests (bucket already in the URL path) pass through unchanged. Use S3VirtualHostFor when OVERCAST_HOSTNAME is a wildcard-DNS name (e.g. "localhost.localstack.cloud") so CDK asset-publisher URLs also resolve.
func S3VirtualHostFor ¶
S3VirtualHostFor returns a middleware that recognises S3 virtual-hosted-style requests.
When hostname is non-empty, requests whose Host header ends with ".<hostname>" are treated the same as ".<localhost>" requests — the leading subdomain is the bucket name.
Example with hostname="localhost.localstack.cloud":
Host: cdk-hnb659fds-assets-000000000000-ap-southeast-2.localhost.localstack.cloud:4566
is rewritten to path /cdk-hnb659fds-assets-000000000000-ap-southeast-2/… .
func ServiceFromCredential ¶
ServiceFromCredential extracts the service name (e.g. "appsync", "apigateway") from the SigV4 Authorization header's Credential scope. Returns "" if not parseable.
func SigV4 ¶
func SigV4(validate bool, secretResolver SecretResolver, logger *zap.Logger, clk clock.Clock) func(http.Handler) http.Handler
SigV4 validates AWS SigV4 signed requests when validation is enabled. Unsigned requests still pass through so emulator-internal endpoints and local no-auth workflows remain usable.
secretResolver optionally resolves per-access-key secrets from IAM. When nil or when it returns no match the middleware falls back to DefaultSigV4Secret ("test") for backward compatibility.
Types ¶
type HostRouteMatch ¶
type HostRouteMatch struct {
// Label is the recognised host segment, e.g. "execute-api". Always a key
// of hostRouteLabels.
Label string
// ID is the subdomain segment(s) before Label, dot-joined.
ID string
// Region is the AWS region parsed from the segment after Label, or ""
// if that segment doesn't look like a region (e.g. the base hostname
// starts right after the label, as with a bare "localhost" base).
Region string
}
HostRouteMatch is a successfully parsed Host-based AWS endpoint address.
func ParseHostRoute ¶
func ParseHostRoute(host string) (HostRouteMatch, bool)
ParseHostRoute parses host (which may include a port) against the AWS `{id}.{label}[.{region}].{base}` grammar using the labels registered in hostRouteLabels. Returns ok=false for path-style requests, IP literals, or hosts that don't contain a registered label.
type HostRouteRow ¶
type HostRouteRow struct {
// Label must be a key of hostRouteLabels.
Label string
// Rewrite mutates r (typically r.URL.Path/RawPath, and optionally the
// request context, e.g. to stamp a region hint) in place so the request
// matches a route already registered for the owning service. Called
// once, synchronously, before chi's router dispatches on the (possibly
// now-different) path. Rewrite should always mutate on a recognised ID
// — even one that turns out not to exist — and let the owning service's
// own handler produce the AWS-shaped not-found/forbidden error; only a
// Host that doesn't match the grammar at all should fall through
// untouched (see AGENTS.md "Routing fallthrough is S3").
Rewrite func(r *http.Request, m HostRouteMatch)
}
HostRouteRow binds one recognised label to the rewrite that adapts a Host-routed request into the owning service's existing path-style route. See the package doc above for the recipe to add a new row.
type SecretResolver ¶
type SecretResolver interface {
ResolveSecret(ctx context.Context, accessKeyID string) (secret string, found bool, err error)
}
SecretResolver resolves the secret access key for a given access key ID. The default implementation looks up IAM user access keys and STS role session credentials stored in the emulator's state store.
If no secret is found, implementations should return ("", false, nil) so the middleware can fall back to the default secret.
func NewSecretResolver ¶
func NewSecretResolver(st state.Store) SecretResolver
NewSecretResolver returns a SecretResolver backed by the emulator's state store. When st is nil the returned resolver never finds a secret so the middleware falls back to the hardcoded default ("test").