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 BucketNameReservedLabel(bucket string) (string, bool)
- func CORS(next http.Handler) http.Handler
- func ClientAddrFromContext(ctx context.Context) string
- func ClientEndpoint(next http.Handler) http.Handler
- func ClientEndpointFromContext(ctx context.Context) string
- func ContextWithClientAddr(ctx context.Context, addr string) context.Context
- func ContextWithClientEndpoint(ctx context.Context, origin string) context.Context
- func ContextWithRegion(ctx context.Context, region string) context.Context
- func DrainBody(next http.Handler) http.Handler
- func HostAddressing(configuredHostname string, rows *[]HostRouteRow, logger *zap.Logger) func(http.Handler) http.Handler
- func HostRouteServiceFor(m HostRouteMatch) (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 ReservedHostLabels() []string
- func ServiceFromCredential(r *http.Request) string
- func SigV4(validate bool, secretResolver SecretResolver, logger *zap.Logger, ...) func(http.Handler) http.Handler
- type HostClaim
- type HostClaimKind
- type HostClassifier
- type HostRouteMatch
- type HostRouteRow
- type SecretResolver
Constants ¶
const ( LabelExecuteAPI = "execute-api" LabelLambdaURL = "lambda-url" LabelAppSyncAPI = "appsync-api" LabelAppSyncRealtimeAPI = "appsync-realtime-api" LabelCloudFront = "cloudfront" LabelELB = "elb" )
hostroute.go implements ONE general grammar + dispatch table for AWS services whose real invoke/control endpoints encode a resource ID (and usually a region) as a Host subdomain rather than in the request path:
{id}.{label}[.{region}].{base}[:port]
Real examples:
myapi123.execute-api.us-east-1.amazonaws.com (API Gateway invoke, v1+v2)
{url-id}.lambda-url.us-east-1.on.aws (Lambda function URLs)
myapi456.appsync-api.us-east-1.amazonaws.com (AppSync GraphQL)
ParseHostRoute recognises this grammar against the fixed set of known `label` tokens in hostRouteLabels and returns the {id} (everything before the first recognised label — dot-joined, so IDs that themselves contain dots are supported) and {region} (the segment immediately after the label, only when it looks like an AWS region — otherwise region is "" and that segment is just the start of {base}).
hostRouteLabels is the single source of truth mapping a label token to the AWS service that owns it. HostAddressing (hostaddressing.go) reads it to classify requests, and detectService (logger.go) resolves the log label from the claim that classification produced, so a request's log label can never drift from what it was actually dispatched to.
---- Adding a new host-routed service ----
- Check the label cannot plausibly end a bucket name. See the guardrail below — this is a correctness requirement, not style.
- Add its "label" -> service-name entry to hostRouteLabels below.
- In router.go, append one middleware.HostRouteRow{Label: "...", Rewrite: ...} to the rows slice passed to middleware.HostAddressing. Keep Rewrite thin: string manipulation of r.URL.Path (and maybe a region context stamp) only, or a call into one small exported method on the owning service (e.g. apigwSvc.HostRouteRewrite) — never protocol/business logic here.
---- Guardrail: labels must not be plausible bucket-name segments ----
A bucket named "my.execute-api" is not addressable in the bare virtual-hosted form, because "my.execute-api.localhost" parses as an API Gateway invoke. That collision surface stays negligible only because every registered label is a hyphenated, AWS-specific data-plane token that nobody ends a bucket name with. Registering a bare common word ("logs", "events", "data") would widen it immediately.
Prefer the AWS data-plane hostname verbatim — "appsync-realtime-api", not "realtime". TestReservedHostLabels_areNotPlausibleBucketSuffixes enforces the hyphenation rule; docs/plans/host-routing-precedence.md §6 tracks making this evidence-based against the generated AWS operation manifest, so the set can only grow when AWS itself adds a service or hostname.
---- Also reads this table ----
- internal/middleware/region.go's regionFromHost: extracts the region hint from this same grammar for SigV4-less requests, which is the only region evidence a host-routed invoke carries. It used to keep a third divergent label list; H5 folded it onto parseHostRouteName, so registering a label below is all a new host-routed service needs to resolve in the right region. It keeps a small disjoint set for regional service endpoints Overcast does NOT dispatch on ("s3" and friends), which must stay out of this table — see regionalEndpointLabels.
Host-route labels, exported so the services that MINT these URLs (serviceutil.HostRoutedURL callers) use the same string the router dispatches on. serviceutil cannot import this package — middleware imports serviceutil — so the label travels as a parameter; these constants are what stops the two sides drifting.
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 BucketNameReservedLabel ¶
BucketNameReservedLabel reports whether bucket carries a reserved host label as a second-or-later dot segment, which is the only position that collides: in "{bucket}.{base}" the bucket's first segment lands at hostname index 0, and ParseHostRoute requires a label at index >= 1 because {id} must be non-empty in every real AWS host-routed shape.
"execute-api" -> "", false (index 0, safe) "execute-api.thing" -> "", false (index 0, safe) "my.execute-api" -> "execute-api", true "my.execute-api.thing" -> "execute-api", true
func CORS ¶
CORS returns a middleware that sets permissive CORS headers on every response and handles preflight OPTIONS requests.
func ClientAddrFromContext ¶
ClientAddrFromContext returns the caller's IP address as stamped by the ClientEndpoint middleware, or "" when the request had none. It travels with the origin through internal dispatch — CloudFormation resolving a `Fn::GetAtt` runs on the deploying caller's context — so a resource attribute is rendered for whoever asked for the stack, not for the emulator.
func ClientEndpoint ¶
ClientEndpoint records the origin — scheme://host[:port] — that the caller used to reach Overcast, so handlers can mint resource URLs the caller can actually dial back.
This matters because several AWS SDKs resolve a service endpoint from a resource URL rather than from client configuration. The clearest case is SQS: @aws-sdk/middleware-sdk-sqs replaces the resolved endpoint with the QueueUrl's origin whenever the two differ and no explicit `endpoint` was passed to the client — and AWS_ENDPOINT_URL does not count, because it is resolved through the endpoint ruleset's Endpoint parameter and never lands on config.endpoint. .NET and Java v1 use the queue URL as the request URI for the same historical reason (the query protocol addressed queues by URL).
A single server-wide origin therefore cannot serve every caller: "localhost" is right for a host CLI and wrong inside a sibling Lambda container, and a compose service name is the reverse. Minting per request keeps every URL dialable by whoever asked for it.
Requests arriving on a real AWS hostname fall through: the context is left unset and handlers use the configured external origin (OVERCAST_HOSTNAME).
The caller's own address is stamped alongside the origin, because a name is not the whole answer for services whose resource is a *container* rather than Overcast itself: an RDS instance answers on its engine port inside the Docker network and on a published port on the host, so which port to hand back depends on which side of that boundary the caller is. The origin cannot say — a split-horizon hostname is used by both sides — but the source address can. See serviceutil.CallerIsSiblingContainer.
func ClientEndpointFromContext ¶
ClientEndpointFromContext returns the origin stored by the ClientEndpoint middleware, or "" when the request had none (background work, internal callers, or a request on a real AWS hostname). Callers fall back to the configured external origin.
func ContextWithClientAddr ¶
ContextWithClientAddr returns a child context carrying the caller's address, the companion of ContextWithClientEndpoint.
func ContextWithClientEndpoint ¶
ContextWithClientEndpoint returns a child context carrying origin, for background goroutines that mint resource URLs outside a request context.
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 HostAddressing ¶
func HostAddressing(configuredHostname string, rows *[]HostRouteRow, logger *zap.Logger) func(http.Handler) http.Handler
HostAddressing returns the middleware that applies the precedence rule. It replaces the former S3VirtualHostFor + HostDispatch pair; because one classification drives both rewrites, they are mutually exclusive branches of one switch and can no longer both fire.
rows is read through the pointer at request time, so callers can register this middleware early (chi requires all r.Use calls before any route registration) and populate the rows later, once the owning services exist.
func HostRouteServiceFor ¶
func HostRouteServiceFor(m HostRouteMatch) (service string, ok bool)
HostRouteServiceFor reports the detectService() label owning a parsed host-route match — e.g. "apigateway" for an execute-api Host. detectService resolves it from the claim HostAddressing stamped on the request, so a request's log label is what actually routed it rather than a re-derivation that could disagree.
func IAMEnforce ¶
IAMEnforce enforces opt-in IAM authorization.
func InvalidateIAMEnforceCache ¶
func InvalidateIAMEnforceCache()
InvalidateIAMEnforceCache marks every compiled-policy cache stale. Called by the IAM service after any operation that changes what a principal is allowed to do.
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 ReservedHostLabels ¶
func ReservedHostLabels() []string
ReservedHostLabels returns the host labels Overcast dispatches on, sorted for stable output. These are "reserved" only in the routing sense: a bucket whose name carries one as a second-or-later dot segment is not addressable in the bare virtual-hosted form (see BucketNameReservedLabel). Bucket creation is never refused — real AWS accepts such names, and both path-style and tier-A addressing keep working.
This is deliberately NOT the set of all AWS endpoint prefixes. Reserving all of them would make names like "my.logs" or "my.events" collide for no benefit, since Overcast does not host-route those services.
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 HostClaim ¶
type HostClaim struct {
Kind HostClaimKind
Bucket string // set when Kind == HostClaimS3
Route HostRouteMatch // set when Kind == HostClaimHostRoute
}
HostClaim is the single verdict for a request's Host header. Exactly one of Bucket / Route is meaningful, selected by Kind — the two can never both be set, which is the invariant the old two-middleware arrangement violated.
func HostClaimFromContext ¶
HostClaimFromContext returns the claim stamped by HostAddressing. Only host-routed claims are stamped: S3 and unclaimed requests both end at the S3 handler, which is already detectService's default, so stamping them would allocate a context on the hottest path for no benefit.
type HostClaimKind ¶
type HostClaimKind uint8
HostClaimKind identifies which addressing scheme owns a request.
const ( // HostClaimNone means no scheme claimed the Host; the request keeps its // path and falls through to S3 path-style routing. HostClaimNone HostClaimKind = iota // HostClaimS3 means the Host carries an S3 bucket as a subdomain. HostClaimS3 // HostClaimHostRoute means the Host matches a registered host-routed // service (execute-api, lambda-url, appsync-api). HostClaimHostRoute )
type HostClassifier ¶
type HostClassifier struct {
// contains filtered or unexported fields
}
HostClassifier applies the precedence rule above. Construct it once (it precomputes the recognised base list) and call Classify per request.
func NewHostClassifier ¶
func NewHostClassifier(configuredHostname string) *HostClassifier
NewHostClassifier returns a classifier recognising the built-in wildcard-DNS bases plus configuredHostname (OVERCAST_HOSTNAME) when set. The configured hostname is additive, never a replacement.
func (*HostClassifier) Classify ¶
func (c *HostClassifier) Classify(host string) HostClaim
Classify returns the single owner of host (which may include a port). Allocation-free.
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. It considers the grammar in isolation. Callers that must also account for S3 virtual-hosted addressing — i.e. anything on the request path — should use HostClassifier.Classify instead, which applies the full precedence rule.
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").