Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Config struct {
Insecure bool `yaml:"insecure"`
AllowInsecureBackends bool `yaml:"allow-insecure-backends"`
ProxyPreserveHost bool `yaml:"proxy-preserve-host"`
ProxyPreserveOriginal bool `yaml:"proxy-preserve-original"`
DevMode bool `yaml:"dev-mode"`
SupportListener string `yaml:"support-listener"`
DebugListener string `yaml:"debug-listener"`
Expand Down Expand Up @@ -456,6 +457,7 @@ func NewConfig() *Config {
flag.BoolVar(&cfg.Insecure, "insecure", false, "flag indicating to ignore the verification of the TLS certificates of the backend services")
flag.BoolVar(&cfg.AllowInsecureBackends, "allow-insecure-backends", false, "enables the per-route proxySSLVerifyOff() filter that skips TLS certificate verification for individual backends; disabled by default")
flag.BoolVar(&cfg.ProxyPreserveHost, "proxy-preserve-host", false, "flag indicating to preserve the incoming request 'Host' header in the outgoing requests")
flag.BoolVar(&cfg.ProxyPreserveOriginal, "proxy-preserve-original", false, "flag indicating to preserve the original, pre-filter-chain request and response metadata, accessible to filters via ctx.OriginalRequest()/ctx.OriginalResponse(); required by any filter that needs the request as it was before path or header modifying filters ran; adds a per-request metadata clone")
flag.BoolVar(&cfg.DevMode, "dev-mode", false, "enables developer time behavior, like unbuffered routing updates")
flag.StringVar(&cfg.SupportListener, "support-listener", ":9911", "network address used for exposing the /metrics endpoint. An empty value disables support endpoint.")
flag.StringVar(&cfg.DebugListener, "debug-listener", "", "when this address is set, skipper starts an additional listener returning the original and transformed requests")
Expand Down Expand Up @@ -1310,6 +1312,10 @@ func (c *Config) ToOptions() skipper.Options {
options.ProxyFlags |= proxy.PreserveHost
}

if c.ProxyPreserveOriginal {
options.ProxyFlags |= proxy.PreserveOriginal
}

if c.RemoveHopHeaders {
options.ProxyFlags |= proxy.HopHeadersRemoval
}
Expand Down
21 changes: 21 additions & 0 deletions docs/operation/operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,22 @@ If the second interpretation gets considered the right way, and the
other one a bug, then the default value for this flag may become to
be on.

## Preserving the Original Request

Filters that need the request or response as they were before any filter in the
route ran can call `ctx.OriginalRequest()` / `ctx.OriginalResponse()`. By default these return
`nil`: cloning that metadata on every request has a cost, so it is opt-in.

Set `-proxy-preserve-original` to enable it on the main listener. The `-debug-listener` (see
[Debugging Requests](#debugging-requests)) always has this behavior enabled internally, regardless
of this flag, since it needs the original request to produce its diff output.

Any filter that rewrites the request, such as
[`modPath`](../reference/filters.md#modpath) or [`setPath`](../reference/filters.md#setpath),
makes the pre-filter-chain request unavailable through `ctx.Request()` to filters
running later in the chain. Those later filters need `-proxy-preserve-original` set to recover it
via `ctx.OriginalRequest()`; without the flag it silently returns `nil`.

## Debugging Requests

Skipper provides [filters](../reference/filters.md), that can change
Expand Down Expand Up @@ -2056,6 +2072,11 @@ flowchart TD
- `cache.reval_dropped`: Counter, revalidation jobs dropped because the queue was full or body read failed
- `cache.reval_error`: Counter, background revalidation fetch failures
- `cache.reval_duration`: Histogram, end-to-end duration of each background revalidation job
- `cache.reval_backend_dispatch`: Counter, revalidation fetches sent directly to the
backend, bypassing skipper's proxy pipeline (force mode, static backend
only) - so they're invisible to `MeasureBackend*` metrics and the access
log. Self-loopback revalidations aren't counted here since they go
through the listener normally.

**L2 (if Valkey or Redis is configured):**

Expand Down
18 changes: 17 additions & 1 deletion docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,12 @@ approach of certain LB implementations.


## HTTP Path

Filters in this section rewrite the request path. Once one of them has run, the original,
pre-filter-chain request is only reachable from later filters through
`ctx.OriginalRequest()`, which stays `nil` unless the `-proxy-preserve-original` flag is set
(`false` by default). See [Preserving the Original Request](../operation/operation.md#preserving-the-original-request).

### modPath

Replace all matched regex expressions in the path.
Expand Down Expand Up @@ -4102,7 +4108,17 @@ routes.

Stale-while-revalidate (SWR): a stale entry within the SWR window is served
immediately while a background fetch refreshes the entry. Concurrent cold-miss
requests for the same key are coalesced into a single upstream fetch.
requests for the same key are coalesced into a single upstream fetch. The SWR
window's source depends on the mode: in force mode it's the operator-supplied
`swrWindow` parameter below; in RFC mode it's the response's own
[`stale-while-revalidate`](https://www.rfc-editor.org/rfc/rfc5861#section-3)
`Cache-Control` directive — if the upstream never sends that directive, RFC-mode
entries have no SWR window and simply expire at TTL. The background fetch is
dispatched directly to the route's backend when possible (force mode with a
static backend), otherwise it loops back through skipper's own listener; see
`cache.reval_backend_dispatch` in the
[operation guide](../operation/operation.md#cache) for the observability
difference between the two.

Unsafe methods (`POST`, `PUT`, `DELETE`, `PATCH`) invalidate the cached entry on
success. `HEAD 200` freshens stored headers without replacing the body.
Expand Down
29 changes: 20 additions & 9 deletions filters/cache/cache_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import (
)

type cacheDirectives struct {
noStore bool
noCache bool
private bool
mustRevalidate bool
proxyRevalidate bool
public bool
maxAge int64 // -1 = not present; 0 means max-age=0
sMaxAge int64 // -1 = not present
noStore bool
noCache bool
private bool
mustRevalidate bool
proxyRevalidate bool
public bool
maxAge int64 // -1 = not present; 0 means max-age=0
sMaxAge int64 // -1 = not present
staleWhileRevalidate int64 // -1 = not present (RFC 5861 §3)
}

type requestCacheDirectives struct {
Expand Down Expand Up @@ -68,7 +69,11 @@ func parseRequestCacheControl(h http.Header) requestCacheDirectives {
// Uses Header.Values to handle multiple header lines; matches names
// case-insensitively per RFC 9111 §5.2.
func parseCacheControl(h http.Header) cacheDirectives {
d := cacheDirectives{maxAge: -1, sMaxAge: -1}
d := cacheDirectives{
maxAge: -1,
sMaxAge: -1,
staleWhileRevalidate: -1,
}
for _, line := range h.Values("Cache-Control") {
for token := range strings.SplitSeq(line, ",") {
parts := strings.SplitN(strings.TrimSpace(token), "=", 2)
Expand Down Expand Up @@ -98,6 +103,12 @@ func parseCacheControl(h http.Header) cacheDirectives {
d.sMaxAge = v
}
}
case "stale-while-revalidate":
if len(parts) == 2 {
if v, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64); err == nil {
d.staleWhileRevalidate = v
}
}
}
}
}
Expand Down
30 changes: 17 additions & 13 deletions filters/cache/cache_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,23 @@ func TestParseCacheControl(t *testing.T) {
header http.Header
want cacheDirectives
}{
{"no-store", http.Header{"Cache-Control": {"no-store"}}, cacheDirectives{noStore: true, maxAge: -1, sMaxAge: -1}},
{"no-cache", http.Header{"Cache-Control": {"no-cache"}}, cacheDirectives{noCache: true, maxAge: -1, sMaxAge: -1}},
{"private", http.Header{"Cache-Control": {"private"}}, cacheDirectives{private: true, maxAge: -1, sMaxAge: -1}},
{"must-revalidate", http.Header{"Cache-Control": {"must-revalidate"}}, cacheDirectives{mustRevalidate: true, maxAge: -1, sMaxAge: -1}},
{"comma-separated", http.Header{"Cache-Control": {"no-store, must-revalidate"}}, cacheDirectives{noStore: true, mustRevalidate: true, maxAge: -1, sMaxAge: -1}},
{"multiple lines", http.Header{"Cache-Control": {"no-cache", "must-revalidate"}}, cacheDirectives{noCache: true, mustRevalidate: true, maxAge: -1, sMaxAge: -1}},
{"case-insensitive", http.Header{"Cache-Control": {"NO-STORE"}}, cacheDirectives{noStore: true, maxAge: -1, sMaxAge: -1}},
{"value suffix stripped", http.Header{"Cache-Control": {`no-cache="x-private"`}}, cacheDirectives{noCache: true, maxAge: -1, sMaxAge: -1}},
{"empty", http.Header{}, cacheDirectives{maxAge: -1, sMaxAge: -1}},
{"max-age=3600", http.Header{"Cache-Control": {"max-age=3600"}}, cacheDirectives{maxAge: 3600, sMaxAge: -1}},
{"s-maxage=60", http.Header{"Cache-Control": {"s-maxage=60"}}, cacheDirectives{maxAge: -1, sMaxAge: 60}},
{"max-age=3.4", http.Header{"Cache-Control": {"max-age=3.4"}}, cacheDirectives{maxAge: -1, sMaxAge: -1}}, // malformed: ParseInt fails, sentinel unchanged
{"max-age=0,max-age=5", http.Header{"Cache-Control": {"max-age=0, max-age=5"}}, cacheDirectives{maxAge: 5, sMaxAge: -1}}, // last-write-wins; no duplicate guard
{"no-store", http.Header{"Cache-Control": {"no-store"}}, cacheDirectives{noStore: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"no-cache", http.Header{"Cache-Control": {"no-cache"}}, cacheDirectives{noCache: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"private", http.Header{"Cache-Control": {"private"}}, cacheDirectives{private: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"must-revalidate", http.Header{"Cache-Control": {"must-revalidate"}}, cacheDirectives{mustRevalidate: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"comma-separated", http.Header{"Cache-Control": {"no-store, must-revalidate"}}, cacheDirectives{noStore: true, mustRevalidate: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"multiple lines", http.Header{"Cache-Control": {"no-cache", "must-revalidate"}}, cacheDirectives{noCache: true, mustRevalidate: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"case-insensitive", http.Header{"Cache-Control": {"NO-STORE"}}, cacheDirectives{noStore: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"value suffix stripped", http.Header{"Cache-Control": {`no-cache="x-private"`}}, cacheDirectives{noCache: true, maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"empty", http.Header{}, cacheDirectives{maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"max-age=3600", http.Header{"Cache-Control": {"max-age=3600"}}, cacheDirectives{maxAge: 3600, sMaxAge: -1, staleWhileRevalidate: -1}},
{"s-maxage=60", http.Header{"Cache-Control": {"s-maxage=60"}}, cacheDirectives{maxAge: -1, sMaxAge: 60, staleWhileRevalidate: -1}},
{"max-age=3.4", http.Header{"Cache-Control": {"max-age=3.4"}}, cacheDirectives{maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}}, // malformed: ParseInt fails, sentinel unchanged
{"max-age=0,max-age=5", http.Header{"Cache-Control": {"max-age=0, max-age=5"}}, cacheDirectives{maxAge: 5, sMaxAge: -1, staleWhileRevalidate: -1}}, // last-write-wins; no duplicate guard
{"stale-while-revalidate=3600", http.Header{"Cache-Control": {"stale-while-revalidate=3600"}}, cacheDirectives{maxAge: -1, sMaxAge: -1, staleWhileRevalidate: 3600}},
{"stale-while-revalidate=0", http.Header{"Cache-Control": {"stale-while-revalidate=0"}}, cacheDirectives{maxAge: -1, sMaxAge: -1, staleWhileRevalidate: 0}},
{"stale-while-revalidate malformed", http.Header{"Cache-Control": {"stale-while-revalidate=bad"}}, cacheDirectives{maxAge: -1, sMaxAge: -1, staleWhileRevalidate: -1}},
{"max-age and stale-while-revalidate", http.Header{"Cache-Control": {"max-age=300, stale-while-revalidate=60"}}, cacheDirectives{maxAge: 300, sMaxAge: -1, staleWhileRevalidate: 60}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
Expand Down
Loading
Loading