# Changelog All notable changes to Aastro are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/). --- ## [0.11.1] - 2026-10-03 ### Fixed - `http2: off` broke every TLS connection with an HTTP/2-capable peer instead of pinning it to HTTP/1.1 - on the data port clients got the connection dropped mid-request, on an upstream every request failed with 502. `off` only restricted `http.Server`/`http.Transport.Protocols`, while the TLS config still hardcoded ALPN `NextProtos: [h2, http/1.1]`, so the handshake kept negotiating `h2` that the HTTP layer then couldn't speak. ALPN now advertises only `http/1.1` when `http2: off`, on both the server and the upstream side. `auto` and `on` are unaffected, as is anything without TLS (no ALPN there, so no HTTP/2 to negotiate in the first place). ## [0.11.0] - 2026-10-03 ### Added - `http2: auto | on | off` on `gateway.server` and per-upstream `transport`. `auto` (the default) is exactly today's behavior: HTTP/2 negotiated over TLS, HTTP/1.1 otherwise - existing configs are unaffected. `off` pins either side to HTTP/1.1 even when TLS is on - the escape hatch for a backend with a flaky HTTP/2 stack, or a quick killswitch on the data port (e.g. for HTTP/2 Rapid Reset-style concerns) without touching TLS config. `on` requires `tls.enabled` at the same scope - the gateway has no cleartext HTTP/2 (h2c) support - and is rejected by config validation otherwise. ### Changed - Internal: upstream transports now set HTTP/2 via `http.Transport.Protocols` instead of the deprecated `golang.org/x/net/http2.ConfigureTransport`, dropping the direct dependency on `golang.org/x/net` (now indirect-only, pulled in transitively). Not user-facing by itself - `http2: auto` behaves exactly as before - but it's what made the explicit on/off knob above straightforward to add on the client side; the data port gets the equivalent treatment via the same `http.Server.Protocols` field. ## [0.10.0] - 2026-08-28 ### Fixed - Retries never fired for an upstream without an explicit `method:` - `retry_on_statuses`/timeout retries were silently a no-op for the common case of relying on the flow's own method. `shouldRetry` judged idempotency against the raw (often empty) config value instead of the effective method used for the actual request. (The 0.9.0 entry below describing this as fixed was itself wrong: the switch cases changed, but the call site never did.) - The `TE` response header was never stripped despite being listed in the hop-by-hop set - the map key was the literal string `"TE"`, but `net/http` always stores and iterates response headers in `net/textproto` canonical form, which for `TE` is `Te` (no hyphen to separate words, so only the first letter is capitalized). The lookup silently missed every time, leaking the header to the client. Every other entry in that set was already canonical. - `policy.header_blacklist` entries were matched by exact string, not canonicalized - `header_blacklist: ["x-secret"]` silently never blocked a response header that `net/http` (correctly) stores as `X-Secret`. Same root cause as the `TE` fix above: HTTP header names are case-insensitive, but the code was comparing them as case-sensitive strings. ### Changed - **Breaking:** flow option `passthrough` renamed to `streaming` (config field, OpenAPI `x-aastro` extension, and `aastroctl openapi import --mode`). Existing configs and specs must rename `passthrough: true` to `streaming: true` before upgrading. - **Breaking:** a flow with exactly one upstream is no longer wrapped in the JSON response envelope. Its upstream's status, headers, and body are now forwarded to the client as-is - on success, and on a client error or redirect (2xx/3xx/4xx), including bodies that aren't valid JSON, which previously got swallowed into an `UPSTREAM_CLIENT_ERROR`/`UPSTREAM_REDIRECT` envelope. Only a genuine gateway-side failure (upstream unavailable, 5xx, timeout, policy violation) still returns a gateway-authored error body - see the envelope removal entry below for its current shape, which by the end of this release also covers multi-upstream flows. Single-upstream flows are now handled by their own dispatch path, bypassing the scatter/aggregate machinery entirely - see `Router.dispatch`/`Router.buildProxyResponse`. - `aggregation` is no longer required for a single-upstream flow, since it's never read for one - only a flow with more than one upstream must declare it (config validation enforces this in place of the old struct-tag check). - `aastroctl openapi import --mode envelope` renamed to `--mode proxy`, matching the response shape above; scaffolded (single-upstream) flows no longer get a meaningless `aggregation: {strategy: array}` block. - Internal: the several independent switch statements over the upstream error classification (retry eligibility, circuit breaker signal, policy applicability, client error code) were consolidated into one table (`kindTable` in `upstream_error.go`), checked exhaustive at startup. Not user-facing, but worth knowing if you're extending upstream error handling: add the new kind's row there rather than hunting down every switch. - **Breaking:** the `{"data": ..., "errors": ..., "meta": ...}` envelope is gone entirely, including for multi-upstream (aggregating) flows - a client no longer unwraps a gateway-specific shape to get at the payload: - A full or partial (`206`, bestEffort) success returns the aggregated data itself as the body - exactly the `merge`/`array`/`namespace` result, nothing wrapping it. Which upstreams failed on a partial success moves to the `X-Partial-Errors` response header (one value per failure, e.g. `UPSTREAM_UNAVAILABLE`) instead of an `errors` body field. - A response with no data at all - every upstream failed, or the request was rejected before reaching one (rate limit, payload too large, plugin failure, single-upstream gateway failure) - is now an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details document (`application/problem+json`): `{"type": "about:blank", "title", "status", "detail"?, "errors": [...]}`. `type` is always the literal `"about:blank"` - RFC 9457's own placeholder for "no further-specific type" - never a real URI, deliberately: a dereferencable type link is reconnaissance, since it names the gateway software fronting the request and invites probing for what that implies about the backend. `errors` is the machine-readable discriminator instead (always present, one entry per distinct underlying `ClientError` - more than one only for a multi-upstream failure with several different causes); `title` is a human summary of the highest-priority one and may change wording over time. `ClientResponse`/`ResponseMeta` are removed from the Go API; `WriteError` and the OpenAPI-generated schemas both moved to `ProblemDetails`. - Request correlation is unaffected by this - it was already carried solely by the `X-Request-ID` response header, never duplicated into a body field. - **Breaking / security fix:** `builtin/middlewares/auth`'s default realm - sent in the `WWW-Authenticate` header on every `401` the gateway itself returns - changed from `"aastro"` to `"restricted"`. Scope note: this is about the gateway's own runtime response to a client. OpenAPI export output and anything that only affects tracing/metrics (e.g. `gateway.service.name`) are a separate concern, owned by whoever runs and publishes them - not touched here. ## [0.9.0] - 2026-08-09 ### Added - `policy.follow_redirects` - per-upstream option controlling whether the gateway follows upstream redirects. Defaults to `false`: a 3xx is now propagated to the client instead of being chased. Set it to `true` to restore the previous behavior, keeping in mind that the redirect is followed with the upstream's own TLS configuration and that Go strips `Authorization` and `Cookie` on cross-host hops. - New client error codes `UPSTREAM_CLIENT_ERROR` and `UPSTREAM_REDIRECT`, distinguishing an upstreams answer to the client from an upstream failure. ### Changed - Upstream responses are now classified by status class rather than the single `>= 500` check. 5xx and transport failures remain gateway failures (502, upstream body dropped); 3xx and 4xx are treated as answers addressed to the client. - Single-upstream flows propagate the upstream status verbatim - a 404, 401, or 302 from the upstream now reaches the client as a 404, 401, or 302. Aggregating flows are unaffected: a status is only propagated when every failed upstream agreed on it, otherwise the response stays 502. - `allowed_statuses` is now enforced as a contract in both directions: a status on the list is always a success (its body flows into `data`), and a status outside a non-empty list is a policy violation returning 502 `UPSTREAM_MALFORMED`, even for statuses that would otherwise be propagated. - Client errors and redirects no longer count as circuit breaker failures - a burst of 404s can no longer take a healthy upstream out of rotation. - Responses with statuses that cannot carry a body (204, 304) are no longer wrapped in the JSON envelope. - Upstream policy validation is skipped for transport-level failures, which previously produced misleading `empty body not allowed` and `status 0 not in allowed list` messages on timeouts. ### Fixed - A single-upstream flow returned **200** with the upstream's error body when the upstream answered 404, 401, or any other non-2xx status outside `allowed_statuses`. Clients received a success for a request that failed. - The circuit breaker reset itself with the very request it rejected: a denied call was recorded as a success, so an open breaker closed after blocking a single request instead of staying open until `reset_timeout`. - Retries never fired for upstreams without an explicit `method:`. Retry eligibility was judged against the raw configured method rather than the effective one, so the empty default was never considered idempotent. - Empty upstream bodies (204, or 200 with no content) were reported as `UPSTREAM_MALFORMED` during aggregation, turning a successful fan-out into a 206 partial response. - Aggregation error responses now carry the retried status correctly; an aborted read no longer produces a redirect-shaped `errors` entry for legitimately empty 3xx bodies. ## [0.8.0] - 2026-08-06 ### Added - `aastroctl openapi import` - generate a gateway configuration from an OpenAPI 3.x document. Documents produced by `openapi export --extensions` are reconstructed losslessly: flows, aggregation, upstreams, policy, and transport are restored, with default-valued fields elided for a minimal, human-readable result. Foreign documents are scaffolded as single-upstream flows, with hosts taken from `--default-host` or `servers[]`. Passthrough flows are detected from streamed `*/*` responses, and the rate limiter is inferred from `429` responses. Secrets are never restored: plugin/middleware configs and TLS certificate paths are reported as warnings for manual re-adding. The generated config is validated before output, so `import` never emits a config the gateway would reject. ## [0.7.0] - 2026-07-18 ### Added - Support for the HTTP QUERY method. - `aastroctl openapi export` - generate an OpenAPI 3.1 (or 3.0) document from a gateway configuration. Response statuses are derived from the actual config: `206` only for best-effort multi-upstream flows, `409` only under `on_conflict: error`, `429` only when the rate limiter is enabled. Flows guarded by the builtin `auth` middleware get a `bearerAuth` security scheme and a `401` response. Passthrough flows are modeled as streamed `*/*` responses. Optional `--extensions` flag embeds `x-aastro` snapshots for future config round-trip (middleware configs and secrets are never serialized). Deterministic output, git-friendly. The config is loaded through the same pipeline as the gateway, so the command doubles as a config validator. ### Changed - Client IP is now extracted only from trusted sources: `X-Forwarded-For` and `X-Real-IP` are honored only when the request comes from a `trusted_proxies` peer, with right-to-left XFF parsing. Prevents rate limiter bypass and client IP spoofing via forged headers. ## [0.6.0] - 2026-07-05 ### Added - **Hot reload of TLS certificates.** Aastro watches the directories of the configured `cert_file`, `key_file`, and `ca_file` paths and atomically swaps the in-memory material when they change. New TLS handshakes use the new certificate; in-flight connections are unaffected. No configuration change is required - rotation works on the existing cert paths. Directory-level watching handles both atomic file replacement on a host (write-to-temp-then-rename) and Kubernetes secret mounts, where the projected files are updated via symlink swap rather than in-place writes. Certificate rotation through cert-manager, Vault Agent, or SPIFFE/SPIRE sidecars is now hands-off. Reloads are validated before they are applied: if a new certificate or CA bundle on disk fails to parse, the error is logged and the previously loaded material stays live, so a malformed rotation cannot take the listener down. ## [0.5.1] - 2026-06-19 ### Fixed - Retries: `retry_on_statuses` is now honored for 5xx responses. Previously any status ≥ 500 was coerced into an error and retried unconditionally, ignoring the configured status list. 5xx and non-5xx are now governed by the same rule. - Retries: non-retryable failures (`internal`, `body_too_large`, `policy_violation`, `circuit_open`, `canceled`, `read_error`) are no longer retried - they previously looped until `max_retries` was exhausted with no chance of a different outcome. - Upstream errors: response body and headers of 5xx responses are no longer discarded. They are now read and preserved on the upstream response, so error detail is available to logging and downstream handling. ### Added - Retries: idempotency guard. Status-based retries now apply only to idempotent methods (GET, HEAD, OPTIONS, TRACE, PUT, DELETE). Non-idempotent requests (e.g. POST, PATCH) are no longer replayed on a retryable status, preventing duplicate side effects. Transport-failure retries are unaffected. ### Changed - Retries: retry decision logic consolidated into a single explicit rule. Transport-level failures (`timeout`, `connection`) are always retryable; status-based retries are driven solely by `retry_on_statuses`; everything else is terminal. - Used goccy/go-json instead of stdlib encoding/json ## [0.5.0] - 2026-05-26 ### Added - New command-line interface and ctl for stub plugins initialization. For more information about the CLI, see the [documentation](https://voidrunner3074.github.io/aastrodocs). ### Changed - Project was renamed from Kono to Aastro ## [0.4.1] - 2026-05-24 ### Changed - Changed final image to chainguard/wolfi-base ### Fixed - Used UUID v7 instead of ULID for request id ## [0.4.0] - 2026-05-16 ### ⚠️ Breaking Changes - The configuration schema has been restructured to separate concerns by listener and responsibility. Existing 0.3.x configs will fail to load with a clear validation error pointing at the missing sections. - **Admin endpoints moved to a top-level `admin` section.** Previously `server.admin_port`, `server.admin_bind_addr`, and `server.pprof` lived inside `server`. They are now grouped under their own section, since admin is a separate listener with separate semantics (never TLS-terminated, bound to localhost by default, distinct timeouts). ```yaml # Before (0.3.x) gateway: server: port: 7805 admin_port: 9090 admin_bind_addr: 127.0.0.1 pprof: enabled: true # After (0.4.0) gateway: server: port: 7805 admin: port: 9090 bind_addr: 127.0.0.1 enable_pprof: true ``` - **Observability moved to a top-level `observability` section.** `metrics` and `tracing` were previously nested under `server`. They are not server concerns - metrics is either scraped from the admin port (Prometheus exporter) or pushed to OTLP, and tracing is always push-only. ```yaml # Before (0.3.x) gateway: server: metrics: { ... } tracing: { ... } # After (0.4.0) gateway: observability: metrics: { ... } tracing: { ... } ``` - **pprof no longer has its own port.** Previously `pprof.port` opened a separate listener. pprof endpoints now live on the admin port under `/debug/pprof/`, controlled by `admin.enable_pprof`. One fewer port to manage and to expose through network policy. - **Flow field `max_parallel_upstreams` renamed to `parallel_upstreams`.** Aligns docs with actual behaviour and the code field name. Same semantics, same default (`2 × NumCPU`). ### Added - **mTLS support, end-to-end.** Both the data port (inbound mTLS) and individual upstreams (outbound mTLS) can now be configured with client certificate authentication, custom CA bundles, configurable minimum TLS version (1.2 or 1.3), and SNI override. ```yaml gateway: server: tls: enabled: true cert_file: /etc/aastro/server.crt key_file: /etc/aastro/server.key client_auth: require # none | optional | require client_ca_file: /etc/aastro/client-ca.crt min_version: "1.2" routing: flows: - upstreams: - tls: enabled: true cert_file: /etc/aastro/clients/users.crt key_file: /etc/aastro/clients/users.key ca_file: /etc/aastro/internal-ca.crt server_name: user-service.internal ``` - **Liveness and readiness probes.** New `/__ready` endpoint on the admin port returns `200` while the gateway is accepting traffic and `503` once graceful shutdown begins. This lets Kubernetes (or any orchestrator with readiness probes) remove the pod from service endpoints *before* the data port stops accepting connections, enabling true zero-downtime deploys. `/__health` continues to return `200` while the process is alive and never checks dependencies. - **Configurable admin timeout.** New `admin.timeout` field (default `5m`) controls read/write timeout on the admin port. Replaces a previously hard-coded constant. The generous default exists to accommodate long pprof captures (`/debug/pprof/profile`, `/debug/pprof/trace`); production data-port timeouts remain short. - **Configurable header timeouts.** New `server.header_timeout` and `admin.header_timeout` fields (default `5s` each) set `http.Server.ReadHeaderTimeout` on the respective listeners. Defends against Slowloris-style attacks where a client trickles request headers slowly to exhaust the server. - **Structured TLS handshake logging.** `http.Server.ErrorLog` is now wired through the gateway's zap logger on both data and admin listeners. TLS handshake failures (failed client certificate verification, version mismatch, unsupported cipher) now appear in the same structured log stream as the rest of the application instead of escaping to stderr via the standard logger. ### Changed - **Admin listener binds to `127.0.0.1` by default.** Previously bound to all interfaces. The admin port carries diagnostic endpoints (`/__health`, `/__ready`, `/metrics`, `/debug/pprof/`) that should not be exposed externally. Set `admin.bind_addr: 0.0.0.0` explicitly if Prometheus scrapes from outside the pod network. - **`/metrics` endpoint moved to the admin port.** When `metrics.exporter: prometheus`, the endpoint is now served on `admin.port` rather than the data port. This means Prometheus can scrape Aastro over plain HTTP without needing a client certificate, even when the data port enforces mTLS. - **Health probe response format changed.** `/__health` now returns `{"status": "ok"}` with `Content-Type: application/json` instead of plain text `OK`. Consistent with the rest of the gateway's response format. - **TLS 1.0 and 1.1 are not selectable.** `min_version` accepts only `"1.2"` or `"1.3"`. RFC 8996 deprecated 1.0 and 1.1 in 2021, and they are disabled in modern clients regardless of server configuration. ### Migration Notes For most users, the migration is mechanical: move `admin_port`, `admin_bind_addr`, and `pprof` from `server` into a new top-level `admin` section, move `metrics` and `tracing` from `server` into a new `observability` section. If you relied on `metrics` being on the data port - point Prometheus at the admin port instead. If you ran `pprof` on a separate port: that port can be closed, pprof now lives on the admin port. Update any internal documentation or scrape configs. If `max_parallel_upstreams` appears anywhere in your configs: rename to `parallel_upstreams`. Behaviour is unchanged. If you used `min_version: "1.0"` or `"1.1"` anywhere: bump to `"1.2"`. Configuration loading will refuse to start with the old values. ### Fixed - **WaitGroup pooling in scatter.** Removed `sync.Pool` reuse of `sync.WaitGroup` values. WaitGroup does not reset to a clean state after use, and pooling it risks counter corruption on edge paths. Allocation cost of a fresh WaitGroup per request is negligible. - **Race on shutdown error reporting.** Fixed a data race in the serve command where the listener error and the main goroutine's shutdown path both wrote to the same `err` variable. Local variable inside the goroutine now passes the error exclusively through the channel. ## [0.3.1] - 2026-05-11 ### Added - Add `WWW-Authenticate` header to auth middleware ### Changed - Golang build version ldflags will now automatically come from the docker meta step outputs - Sliding window rate limiter instead of fixed-window ### Fixed - Added missing masker plugin and cors middleware to image ## [0.3.0] - 2026-05-04 ### Added - Distributed tracing via OpenTelemetry OTLP/HTTP (`gateway.server.tracing` config block) - Service identity via `gateway.service.name` config and `-ldflags "-X main.version=…"` injection - W3C TraceContext + Baggage propagation, installed unconditionally - `X-Request-Fingerprint` response header and `aastro.request.fingerprint` span attribute for correlation across observability channels - `aastro plugin init` CLI command to create a plugin or middleware skeleton ### Changed - The gateway in docker container is now running as a non-root user - sdk.Plugin.Init() should now return an error - Used [Ginkgo](https://github.com/onsi/ginkgo) for tests ### Fixed - Passthrough flows no longer broken by `client.Timeout` (separate streamClient without timeout) - Passthrough flows no longer broken by `http.Server.WriteTimeout` (per-request `ResponseController.SetWriteDeadline(time.Time{})`) - Client disconnect during passthrough no longer logged as upstream error --- ## [0.2.0] - 2026-04-26 ### Added - **Passthrough mode** - new `passthrough: true` flow option proxies requests directly to a single upstream without buffering or aggregation. Designed for Server-Sent Events (SSE), chunked transfer, and any long-lived HTTP connection. Request plugins still run; response plugins are skipped. - **OTLP metrics exporter** - metrics can now be pushed to any OpenTelemetry-compatible backend via `exporter: otlp`. Previously only Prometheus pull-mode was supported. - **Namespace aggregation strategy** - new `strategy: namespace` places each upstream response under its name as a key: `{"profile": {...}, "stats": {...}}`. - **Response meta envelope** - all gateway responses now include a `meta` object with `request_id` (ULID) and `partial` flag alongside `data` and `errors`. - **`aastro viz` command** - CLI command that renders a visual tree of all configured flows and upstreams using terminal color output. - **JWKS background refresh** - the `auth` middleware now refreshes JWKS keys in the background on a configurable interval (`jwks_refresh_interval`, default 5m), reducing on-demand refresh latency during key rotation. - **`sdk.Closer` interface** - middlewares that hold background resources can implement `Close() error`. Aastro calls it on shutdown via `Router.Close()`. - **Configuration defaults via struct tags** - upstream timeouts, transport pool settings, and server timeout now use `default:` tags powered by `creasty/defaults`, eliminating the manual `ensureGatewayDefaults` function. - **`aastro validate` command** - validates the configuration file and reports human-readable field-level errors without starting the gateway. - **Full hop-by-hop header filtering** - `Keep-Alive`, `TE`, `Proxy-Authenticate`, `Proxy-Authorization`, and `Upgrade` are now stripped from proxied responses in addition to the previously filtered headers. - **`cors` built-in middleware** - Cross-Origin Resource Sharing support with configurable origins, methods, headers, credentials, and preflight cache. - **`ClientErrAborted` error code** - new `ABORTED` error code and `503 Service Unavailable` status for requests cancelled by the client before completion. ### Changed - **`dispatcher` renamed to `scatter`** - internal component renamed to better reflect the fan-out pattern. No user-facing configuration change. - **Upstream policy validation moved into upstream** - `requireBody` and `allowedStatuses` policy checks now run inside `upstream.call()` after the circuit breaker update, so policy violations do not affect circuit breaker state. - **`aggregation` is now optional for passthrough flows** - the `aggregation` block may be omitted when `passthrough: true`. Configuration validation enforces this. - **`pprof.port` only required when pprof is enabled** - previously the validator required a port value unconditionally. - **`AggregationConfig` is now a pointer in `FlowConfig`** - allows the validator to correctly apply `required_if=Passthrough false`. - **`Router.flows` and all internal types unexported** - `Flow`, `Upstream`, `AggregatedResponse`, `UpstreamResponse`, `UpstreamError`, and related types are no longer exported. Public API is limited to `Router`, `NewRouter`, `RoutingConfigSet`, config types, `LoadConfig`, `ClientError`, `ClientResponse`, and `WriteError`. ### Fixed - `passthrough` field not being set on compiled `flow` struct - passthrough flows were silently falling back to buffered mode. - `trackingWriter` not forwarding `Flush()` - SSE events were buffered until connection close instead of being flushed after each chunk. - `headersAlreadySent` always returning `true` - used `http.Flusher` type assertion which is satisfied by almost any `ResponseWriter`, making the double-write guard ineffective. - `Content-Length` forwarded from upstream in passthrough mode - conflicted with chunked/streaming responses and caused client parsing errors. - Circuit breaker state not updated correctly on `HalfOpen` success and `Open` failure - missing `case` branches in switch statements. --- ## [0.1.0] - initial release - Core routing with chi - Fan-out dispatch to multiple upstreams - `merge` and `array` aggregation strategies - Circuit breaker, retry, load balancing per upstream - Prometheus metrics - Plugin and middleware `.so` loading - Built-in plugins: `camelify`, `snakeify`, `masker` - Built-in middlewares: `auth`, `logger`, `recoverer`, `compressor` - `trusted_proxies` and per-IP rate limiting - YAML configuration with validation