diff --git a/CHANGELOG.md b/CHANGELOG.md index e78c70a..c102000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,12 @@ Release history focused on **features, settings, default behavior, compatibility ## 0.34.7 — Varnish-compatible cache lookup (source candidate) -- **Settings → Cache → Varnish-compatible cache policy (`varnish_compatible`): ON by default**, including for existing configurations without the new field. OFF preserves the legacy stricter interpretation of incoming `Cache-Control` and `Pragma`; the toggle is applied live (no service restart), invalidates existing cache entries for storage-policy safety, and affects both reverse-proxy and embedded Web Server cache, HOT and COLD. -- Varnish-style lookup ignores client `no-cache`, `no-store`, `max-age`, `min-fresh`, `max-stale`, `only-if-cached` and `Pragma` **only as cache control signals**; it does not remove request headers from forwarding, Vary keying, downstream conditions or user identity. Hard reloads of a fresh asset remain `HIT`, retaining `Age` and counters instead of refetching without origin validators. -- For **origin responses**, default-compatible mode rejects `Cache-Control: no-cache`, `Surrogate-Control: no-store`, and responses with no remaining TTL. Previously protected `private`, `no-store`, `Set-Cookie`, Authorization, Vary and all explicit RustProxy cookie exceptions remain intact; an eligible stale object may be served during up to 10 seconds of background-refresh grace unless RustProxy's stronger revalidation restrictions apply. -- Scope deliberately excludes full Varnish/VCL or hit-for-miss emulation and the 120-second implicit origin TTL default; RustProxy keeps its configured default TTL and existing safety limits. To compare live behavior, enable diagnostic headers and inspect `Age`, `X-Cache`, `X-Cache-Hits` and `X-Cache-Reason` before/after hard reload. -- **Delivery:** source candidate, requiring native Rust compilation, integration and production acceptance tests before deployment. No binary release is claimed. +- **Settings → Cache → Varnish-compatible cache policy (`varnish_compatible`)** stays **ON by default**, including upgraded configurations without the field. An adjacent **i** popover (PL/EN) explains its effects, limitations and safety controls. OFF retains strict client hard-reload behavior. The live toggle invalidates existing entries across HOT/COLD. +- Only client hard-reload `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` are ignored when considering a **fresh HIT**, retaining `Age` and hit counters. Kept the original `Cache-Control: no-store` bypass, `only-if-cached` 504 on cache MISS and no background origin refresh on HIT, and `min-fresh`/`max-stale`/positive `max-age` freshness constraints (including explicit `max-stale=0` during grace). Original request headers, Vary, access/privacy rules and Web Server handling remain intact. +- **Origin `no-cache` regression fix:** ETag/Last-Modified entries remain cacheable for conditional revalidation when fallback TTL is disabled. With **explicit positive fallback TTL**, only origin responses marked **`public`** may be reused for that TTL (capped by max TTL), even without origin validators. This is an explicit administrative override of origin `no-cache`, not the default. `private`, `no-store`, `Set-Cookie` and compatibility-mode `Surrogate-Control: no-store` remain vetoes. +- **Zero TTL regression fix:** keep expired representations for revalidation, client `max-stale` and configured `stale-if-error`; an expired object does not become a fresh HIT. Implicit grace defaults to **60 seconds**, can be set in Settings → Cache (`varnish_grace_seconds`, 0 disables it), and applies only to eligible objects stored fresh with no `stale-while-revalidate` directive; explicit `stale-while-revalidate=0` is never overwritten. +- **UI layout:** nested Settings tab bars no longer stick over Cache checkboxes while scrolling; the top-level Settings navigation remains sticky. +- Source-level regression cases cover hard reload, fallback TTL, validator revalidation, no-store/private/Surrogate protections, zero-TTL stale-if-error, min-fresh/max-stale, and `only-if-cached` for proxy and Web Server. Native Rust compilation and runtime acceptance are required before a production release claim. This remains a Varnish-inspired policy subset, not a VCL clone. ## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules (source candidate) diff --git a/README.md b/README.md index a2ec2f6..c103f07 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Features and default settings vary by component; configure only the capabilities ## 0.34.7 -- **Settings → Cache:** Varnish-compatible cache policy (on by default), with legacy strict semantics when disabled. Prevents client hard-reloads from needlessly refilling still-fresh HOT/COLD objects; respects origin cache privacy and preserves RustProxy static-cookie handling. See [Cache](docs/CACHE.md) for the scope and important exceptions. +- **Settings → Cache:** Varnish-inspired cache policy (on by default) ignores hard-reload `no-cache` / `max-age=0` / `Pragma: no-cache` for fresh HITs, without disabling `only-if-cached`, `no-store`, `max-stale` or `min-fresh`. Public origin `no-cache` responses can use an explicit fallback TTL; other responses retain revalidation, `stale-if-error`, Surrogate-Control vetoes and privacy protections. The behavior is explained by an **i** icon next to the setting. See [Cache](docs/CACHE.md) for examples and safety implications. ## 0.34.6 diff --git a/SHA256SUMS b/SHA256SUMS index 9330110..3514ed8 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -2,11 +2,11 @@ 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 10e5382f535df61cc2e5905d83a61d40ac3e60572e7ff8e05bad6271d871b6c5 AGENTS.md -d88bcb686fed1626510867d9b44a9db4e6020a65e9c2369ced781e5744f64022 CHANGELOG.md +71b520eb7169feb726ba057417156bd74faa71dc4cbd3e3f994bcfee19f851fd CHANGELOG.md 32a192c94173ec66cce81890c71f235febeda7cf59bc8581fc982e1a0759b296 Cargo.lock 82922877469166aff8eec6805e56f9c7f07b95dba2dc02d17c9c9fd5f39747fa Cargo.toml 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile -626f18ec4028ae66ec4b65eb8dab83405e568d64396a3e58d25a247851fecb90 README.md +b9a2fb9e884f26840fbd9c84b1564682ef2093a08b32a502b9b19711c19a3724 README.md ae6d14ada6fde7d0b8514840b821678418cf5b1816c18434e3b7fb2ae27a0b21 ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml 7309cfaa188c7a1fcf30be1b8313ee99c00b7fe5d6d626ddefcc7345fa066112 build.rs @@ -32,8 +32,8 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n 9268ab7fe86683dacd64bafbd9e0edc8956ce9b9ef6277cc6ada61c3fcb84746 docs/API.md 2362ab34020d4e0dcd70e280e9539ef7764b63212856ee08a3a55906c48f6421 docs/API_ACCESS.md 728b15b9825f547f34c4121219784798dd046fbbdd72ece8906d0cd77e8efc64 docs/BACKUP.md -e072271ddba43e3759b9ea7702c41a8357d13008f8b664c62fa860eadf2f1e1e docs/CACHE.md -c3f4cb4e32d90a2f334d864cc3fb051f6a7d662f37daa554a8988530b6dec56f docs/CONFIGURATION.md +6dcfa83377d10169a3b6cd7e6f79c500f0707242874f2d21212be7ca6a81d3fa docs/CACHE.md +f24767c2f9d4db4db5005b700d03323dd20f0a1d180ae939be4fe54a126aaf32 docs/CONFIGURATION.md 80bcc666736b10f6202386d0342e8d6feeba858d77aea310bf3ec6de01c2bfb3 docs/CONTAINER.md dc670d912a6a946160c2f8528c9bb710ec38ebe406dcb7dcaf18fb2653f667b6 docs/CUSTOM_LOCATIONS.md 9fb2d0bcac70d1883dd16557622851a1f0527b3d3569a4b5177f5e6dedd3c74d docs/DASHBOARD.md @@ -76,8 +76,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 13189fa546c3488dddd8efc80c47f6da5241e5fc28b79cbfe795414ea05c7a4f langs/README.md -d08c2f6ea2abc0fc848300c2bfd911885863de81bceda533d10991cc55a93e97 langs/en_US.json -1cf4e525442e65e6007c54304197e71853b8991d9a5990dfdae6e72b1de389e1 langs/pl_pl.json +c04792fcc2e2e237b28e22d64f2db44964f26b779db09622f2162d0f068d5526 langs/en_US.json +95564b46ad8526610dc04d5696b4f26d4c5b46fdbd38290c4850d57d45e90c49 langs/pl_pl.json a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql @@ -143,7 +143,7 @@ d58a2ed83319176de4e66790ed292447c9497ae031464ec55cfe27174acb639a src/admin/file 282352eda7957477e7da44f16b90984b26a4a67a8755833bc0a6b34eaf93e3af src/admin/file_logs_query.rs f66ff718f82a326f80808b0a5088bd05a48625be802e5ba305fff3c344b59948 src/admin/logging_api.rs 1ba0520c6348246cbb35b60e596e99dc600137c0696de86508cf06587bd86563 src/admin/mod.rs -4cd8f528ef3be20d7b58c928e368afca1c4edc4db22840d2e9effc309aa43bcd src/admin/openapi.json +228560ed081bce7a490f401404083c79ecefcdfadac54a7256396ef2f40c0da6 src/admin/openapi.json 7cfd6593ac9bcea3d1b9e61bbcb7ed98d65ed3190f54f94c1e104541f4fbb4f5 src/admin/operations.rs f14afb6ae141053d83d0dc59df835b20e38a37a9e83f86f0f2053f2bd5d9ce57 src/admin/panel_assets.rs f5127855c2994d052678865df36d703b74f4a0afc633daf7718b10da18d26e37 src/admin/panel_content.rs @@ -179,7 +179,7 @@ d997f62e8bd7ac9d4f16bb302800c98ad943afab663bb0b212ea26c35b58de11 src/admin/web/ 43f4dddd1c3ea7fa42eeb9d7b655a8db48b5ddcc6c68213e08dfc2fcfbe8b17d src/admin/web/app/refresh.js f1a764691f3e2cdb7c6c43e37dae78249561c017a1b74e973a378cb780b72835 src/admin/web/app/runtime.js 31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js -4596247d22cefe250345240942807fbcfebf4e9393457c37d4a97078f255e86c src/admin/web/app/settings.js +fa00bce2651308a4e0b1303cc0146be17ed4794b67b2f0cebfaf861558d8c78c src/admin/web/app/settings.js 8386710827e5ccc9432fb46413368f94afc1016425404a6092b460903636837e src/admin/web/app/stream-dashboard.js 7f89bfae66e2a08a05a112733864e908f036bb1ee3d8b5b9ff23783b870acd96 src/admin/web/app/web-overview.js 849b2ca9d183bdf6cb1e04651cda52da55b8a631e11ab1d827a052c8436bbd76 src/admin/web/cache-panel.js @@ -207,7 +207,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/ 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js -efe0560058dc4addd71f69a9642088f06c245099920b7511caa538adf4684443 src/admin/web/index.html +43ea73b9f4e87025aa5c74cbe6062bf419b44956ea929d9132088583d5bf2dde src/admin/web/index.html 7915a06cebb3dfa2c51833c326586920691ba4431b1f984429b039333f9c2a93 src/admin/web/integrations.js 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html @@ -220,7 +220,7 @@ a8afc6e70950bc70b98725eb049d73cff35dc3a3ee659229a15a8e08d3eb4f12 src/admin/web/ be9b78bee4ef0026608191d36a2aed831f93c1baaa5bc73da86bf09bc217ce76 src/admin/web/settings.js 8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html 6feb1996b1caa9d19e0dc56eae7ca5b48b7e7cdf0a93bb2e4b527fb8f13a2fe5 src/admin/web/streams.js -9e135460e63f2f46568f0a30a4e32c88f16d57f817fa2528b79577d7db70fc3f src/admin/web/style.css +642b6e14cef880f7fb2b93644234e82db4e5392ff9e871c08becc8ae9ecf8e26 src/admin/web/style.css d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js dfb4ded2fefb90fc1e2d33370cdef10b040a85cb43235ea9dbc1d73bef823a79 src/admin/web/transform-panel.js a55f45dad5b540def419f8487161fb506b7e52744c76a5e661c7a5fa0a98a426 src/admin/web/vhosts/core.js @@ -277,21 +277,21 @@ e9c6ab0c2be9f96573f826e6a1b9e4c2b8b526f779e72e95a6463ae07e45ab55 src/cache/look 0c8b0d550e94d4474dcdf991aa0340228dc5f2f904f39350e6e666a16f16fe87 src/cache/model.rs c409174a9aaf20c69dc7b329108d1c26b5672a7341236cf639480df48800d262 src/cache/model/cache.rs 0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs -000d82a19cda1501f12a8ef539c1e807529e104a77181fe541738fe6b22908c4 src/cache/model/entry.rs +60b186c980afec29fc867a32b07b1360d9b68ba471ad1f9b8ada29ad309b1414 src/cache/model/entry.rs ff1c3a1a3bc0d26218dcc943ef951c2a212c180bbf6e4bf1c542ffbf12a9f150 src/cache/model/http_invalidation.rs dc1153685671be70c28bef814d72d8ef09afe070598f528ea62260c54d43b26f src/cache/model/maintenance.rs -e9a17446038508275e72a85bf2a4fdc03aae1f8da98433f57de902a8aa22171b src/cache/model/settings.rs -c2efe75f740ba31e83c49bf76e7fc179e897f41c140eea700e2675ae648e4a1c src/cache/model/views.rs +19aaf66abe2ad30b973131a020b7ca8ec6d0646a51df5c604c5f8914db898416 src/cache/model/settings.rs +395a82091ae520b78fa606553a0ec590ffb14540b83234cef5bbdcd2e57d39d5 src/cache/model/views.rs e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs -b6dd187fe9a6f540653c9b7a0bac4e789f1c606b58bf99691e027bce776322ea src/cache/policy/request.rs -130ebd918dace9552eee4f9633b8ad3fe5a1b7945bb00ae6f6683017cf10f527 src/cache/policy/response.rs +896b1869f94973b30a0de0faa62a2c89fbc8991b02de0e5988ab4ce1f77b392d src/cache/policy/request.rs +9dd790a35dbc15df8465664ed9fff296fe22847371d667aefe320504ab952b2b src/cache/policy/response.rs e59efd322f518ebd7b5b154115188f382c2cf7882ca0a401736ba5d685981ea5 src/cache/policy/scope_tests.rs 2b556ac9179b8a92de368f5e907e4114c2fc21a89155fe2a05665c45c08b3883 src/cache/range.rs e1d3f0512d6290b0ac4998f47398a0a7c404db249c5bbdbc4ad4767071013a72 src/cache/stats.rs 8a300f5dad44815401eb23f4f5a157893761852ba4f3005041e0eb3f16892ce3 src/cache/tests.rs -d0f7a9726c0a06553c91ca4d29eca207e9a9c3201fab371bec9dc5f6457ba12e src/cache/tickets.rs +895b589b9866fafafce9f464c4a0d3d2ced55989a1d909bc152ab1529c2a1550 src/cache/tickets.rs b7c063ed4c8ca96714e1dc5d279f38fc521da2132faad7bd4f40311352a71e7c src/cache/v022_cache_tests.rs -6e503de08f3ea67608e534f5ceff58173eb2351d2fa7777e8fb646ed1c5bb7ce src/cache/varnish_compatible_tests.rs +452ff2f162f70c3e4d9259de4b13a7751874742c6922c5e0e0196eeadee4ccaa src/cache/varnish_compatible_tests.rs 5a8022f6f4721f9fabdc715e01127d9299d47ee9981a79d26df1981448487d20 src/compression/mod.rs 2bb1ea392dc18553a03a6ae1c53f52712ae86571ea31b1bcd93c8fdec2cb60ec src/compression/tests.rs e695079333ebdc2a6e7b3337cfabef171b3a0a01da0597b77feff70d417f833e src/config.rs @@ -375,15 +375,15 @@ d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/cach a03b1735daa8fa8e40cf5bd9c6e200d211baf19c30f62522d4237ed56642bce7 src/proxy/cache_plan/mod.rs 3404b03096862de1050088f62cdd811ea1ab61d191f902501173107f81f23b9b src/proxy/cache_plan/tests.rs a0b9e2e9ab0bc89c7b64bd19365198234b1c5c13f455ea47be122a5660639b17 src/proxy/error_fallback.rs -fee36419c7e308cedff3b73ed1e8c5fab1fa054aaa6e0ccc016dd0b043c32c42 src/proxy/forward/cache_fast_path_tests.rs -a20cd11a504e423e5b1af5cebb5b4186530d4eaacaa02c3212a1174cdbfb6498 src/proxy/forward/cache_lookup.rs +34a5c28cf8b719d39561bd0fc28f3ae40505d6ca8fc6b1532e5ea9ce318e7363 src/proxy/forward/cache_fast_path_tests.rs +6b1ae807b0efa480207d9c5baf6e86b1f9dc6becb52600f14e4d1a84e8efcda1 src/proxy/forward/cache_lookup.rs 287012c81144d8f15af4a148ce3ca911d431bc59adff44e697567561aa372bb3 src/proxy/forward/h3_navigation_tests.rs ce97cfcb07d90415937f0520ee7175502b22e60694c1ea8e3d2b47678d190e5f src/proxy/forward/response.rs -f3c3b8c979c06bd84a72969f2908be1052097e32f3a21f0494dbd3ce72f17796 src/proxy/forward/route_cache.rs +1a7322aadbe90e2a6940a27882c13e274b1c92f14de3ebad717054a9614d4ba8 src/proxy/forward/route_cache.rs 3f91a40a01378d1fbac56ec376df6626e0c4fb6c3b8c8d42583b9c398d368ad9 src/proxy/forward/success_finish.rs aa30766a5d584c2143488f16caef19c1d369a2c7c00b5ff960eaf2ccf4045c31 src/proxy/forward/success_prepare.rs e9ac9be64f6f83c4ca2cd5d299649f24dd16197712083c6dc10e55c662c65b80 src/proxy/forward/upstream.rs -de548bd191c57b76b595f0a932195d303b43355982080a185c44f1973d00702c src/proxy/forward/web_source.rs +b7b46ef0c530a0e9d541238e50ec848d80c799b363fed30e0acbccdd459b0ffa src/proxy/forward/web_source.rs 3340583d395fcc859cf3421f4e33cf8347915ee4c468f7d0e4abebce8156fdca src/proxy/forward_entry.rs 9b25f5e3ea4443a63ee6f45d12e1957dd316fe8e85325fc00ea416faaab1025a src/proxy/h2_graceful.rs 5b07b98d855453d5a4272f5fc98a75973c3e6032416e0fe4f7f151c1bf239fd8 src/proxy/headers/authority_tests.rs diff --git a/docs/CACHE.md b/docs/CACHE.md index cb4e9c7..b2dd498 100644 --- a/docs/CACHE.md +++ b/docs/CACHE.md @@ -12,24 +12,29 @@ RustProxy provides an optional shared HTTP response cache with a bounded in-memo ## Varnish-compatible cache policy (0.34.7) -**Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`, **on by default**) changes *request-side* cache lookup, not authentication or the cache key. Disabling it restores the earlier strict policy without a service restart. **Changing this policy invalidates cached representations** to avoid mixing storage semantics across modes; allow time for HOT/COLD to refill. Effective settings also apply to eligible embedded Web Server resources and HOT/COLD entries. Existing JSON configs with no new field inherit `true`. +**Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`, **enabled by default**) avoids needless cache replacement on browser hard reloads. On a fresh object it ignores only the client's `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` for cache **lookup/freshness**. It does not remove or modify these request headers when fetching from the origin, and it does not alter cache identity or request conditionals. -| Condition | Enabled: Varnish-style | Disabled: legacy strict | +Set **Default grace (seconds)** in Settings → Cache (default **60**, range 0..604800). This affects only eligible objects in compatibility mode and never overwrites explicit origin `stale-while-revalidate`, including `=0`. Use the adjacent **i** information icon for the short operator explanation (PL/EN). Disabling the option restores the original strict client reload semantics. Changing modes invalidates currently stored representations across HOT/COLD so they are refilled under one consistent policy; no process restart is needed. Existing JSON configurations with no field inherit `true`. The setting also applies to eligible resources of the embedded Web Server. + +| Condition | Enabled: Varnish-style optimized, RustProxy protections retained | Disabled: legacy strict | |---|---|---| -| Client `Cache-Control: no-cache`, `no-store`, `max-age=0`, `min-fresh`, `max-stale` | Ignore client cache directives for lookup and freshness; a **fresh** object stays HIT and keeps `Age` and hit count | Revalidate/bypass or constrain freshness as instructed by the client | -| Client `Pragma: no-cache`, `only-if-cached` | Ignore both; do not force backend or a synthetic 504 purely because of these directives | Revalidate or give 504 for a missing only-if-cached representation | -| Origin `Cache-Control: no-cache` | Do not store, like Varnish's built-in VCL | Store only with a validator and require revalidation per request | -| Origin `Surrogate-Control: no-store` | Do not store | Existing RustProxy behavior | -| Origin zero/nonpositive *remaining* TTL | Do not store | May retain a zero-TTL representation for revalidation; never a fresh HIT | -| Expired but eligible object | Allow up to 10 seconds of background-refresh grace (or explicit larger `stale-while-revalidate`), unless `must-revalidate` / `proxy-revalidate` forbids stale delivery | Only explicitly configured stale/grace conditions | -| Origin `private`, `no-store`, `Set-Cookie`, `Vary: *`; client Authorization | Existing RustProxy restrictions **always** apply | Same | -| Static-file cookie-ignore allowlist, custom bypass/force rules, TTL caps, BAN/PURGE, request collapse, compression/transform, HOT/COLD, tenant isolation | Unchanged RustProxy mechanisms | Same | +| Client `no-cache`, `max-age=0`, `Pragma: no-cache` | **Fresh HIT** without replacing object or resetting `Age` / HIT counter | Can force origin revalidation (and `200` replacement without an origin validator) | +| Client `no-store` | **BYPASS**: do not use or populate a shared representation | Same | +| Client `only-if-cached` | **Fresh/stale eligible HIT**, or **504** if no reusable representation; never fetch origin on MISS | Same | +| Client `min-fresh`, `max-stale`, positive `max-age` | Keep constraints; an otherwise fresh object can be rejected by `min-fresh`, while eligible stale entries can be served by `max-stale` | Same | +| Origin `no-cache` with `ETag`/`Last-Modified`, fallback TTL = `0` | Store for **conditional revalidation** (`304`) but never fresh-HIT without checking origin | Same | +| Origin **`public, no-cache`** and explicit fallback TTL `> 0` | Treat the administrator's configured **Fallback TTL for public responses** as shared freshness (capped by max TTL), even when the origin has no validator; this explicitly overrides origin `no-cache` | Validator-only revalidation, no override | +| Origin `max-age=0` / already expired, with `stale-if-error`, a validator or client `max-stale` | Retain the expired representation; it does not become a fresh HIT. Can serve stale on an eligible error, revalidate, or honor explicit client allowance | Same | +| Origin `Surrogate-Control: no-store` | Do not store, even when a fallback TTL is configured; this is an additional compatibility-mode safeguard | Legacy response policy | +| Origin `private`, `no-store`, `Set-Cookie`, invalid `Vary` | Must not enter shared cache; configured static-cookie exceptions do not override response privacy | Same | +| Eligible cached object after TTL | Implicit background-refresh grace up to **60 seconds by default** (configurable with `cache.varnish_grace_seconds`, `0` disables it) **only** for entries initially fresh with **no** origin `stale-while-revalidate` directive; explicit `stale-while-revalidate=0` disables implicit grace. Existing `must-revalidate` / `proxy-revalidate` restrictions still apply | No implicit grace | +| Cookies, Authorization, `Vary`, per-vhost exclusions/force, query/range restrictions, BAN/PURGE, request collapse, transforms, compression, HOT/COLD | Existing RustProxy mechanisms remain in effect | Same | -For `/ui/style.css` with `Cache-Control: public, max-age=600` and **no** origin ETag/Last-Modified, a hard reload with `Cache-Control: no-cache` should now remain `HIT` while its cached representation is fresh. The request headers are *not* stripped before forwarding when an origin fetch actually occurs; RustProxy only ignores them for its own lookup/freshness checks. Client `If-None-Match`/`If-Modified-Since`, range conditions, URL query, `Host` and `Vary` are preserved. Strict mode remains available for installations requiring clients to force validation. +**Important:** using a positive fallback TTL to reuse an origin response marked `public, no-cache` is an **explicit administrative freshness override**. It can serve outdated content for that duration; leave fallback TTL at `0` if the backend's `no-cache` must always be honored. The fallback override applies only to responses carrying `public`, not to private/no-store/Set-Cookie responses or `Surrogate-Control: no-store`. The configured `max_ttl_seconds` still caps remaining freshness. Without fallback TTL, origin `no-cache` still requires an origin validator to be stored. An origin `max-age=0` is never made fresh merely because `stale-if-error` is present. -This is **a documented policy subset, not a Varnish binary/VCL clone**. RustProxy deliberately keeps an explicit zero-by-default fallback TTL (no implicit Varnish 120s TTL), configured cookie exceptions, multi-tenant security, stale prohibitions for `must-revalidate`, and no Varnish-style hit-for-miss/hit-for-pass markers. Not every Varnish configuration behaves like the default built-in VCL. Cookies continue to be omitted from the cache key *only* for the already-configured static-file exception; do not enable that exception for personalized static endpoints. A no-cache **origin response** must not be made cacheable merely to gain HITs. +This is **a Varnish-inspired policy subset, not full Varnish binary/VCL compatibility**. RustProxy deliberately keeps an explicit zero-by-default fallback TTL instead of Varnish's implicit 120-second default, retains client controls beyond hard reload, enforces multi-tenant and cookie/Authorization safety rules, and does not implement hit-for-miss/hit-for-pass markers. The current compatibility-specific `Surrogate-Control` safeguard handles `no-store`; it does **not** interpret every Surrogate-Control extension or use Surrogate-Control `max-age` as a TTL override. Different VCL configurations can behave differently. -Use a three-request GET comparison with `curl` (replace URL with your static resource): +To verify with real GET requests (not `curl -I -X GET`): ```bash URL=https://example.com/ui/style.css @@ -38,7 +43,7 @@ curl -sS --compressed -H 'Cache-Control: no-cache' -H 'Pragma: no-cache' -D - -o curl -sS --compressed -D - -o /dev/null "$URL" | grep -iE '^(HTTP/|age:|x-cache:|x-cache-hits:|x-cache-reason:)' ``` -Enable diagnostic headers if needed. With compatibility **on**, the middle response should be `HIT`, and Age should not reset on a fresh object. With compatibility **off**, it can be `MISS` and reset if no origin validator is available. Explicit PURGE/BAN and stale/expired TTL remain authoritative. +With the policy enabled, the middle request stays `HIT` if the entry is fresh. Check safeguards separately: an uncached URL with `Cache-Control: only-if-cached` must return **504** without reaching the backend; a stale `only-if-cached` HIT must **not** start background validation/proactive refresh. `min-fresh` and `max-stale` still constrain stale reuse: an explicit `max-stale=0` must not be bypassed by the configured implicit grace. `Cache-Control: no-store` must bypass an existing fresh entry. ## Vhost editor options diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 10d89d0..407e264 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2,7 +2,7 @@ Most settings are managed in the authenticated panel. Save changes after validation; use **Configuration history** to compare or restore an earlier revision. Some listeners are process-owned and require restart to change. Configuration updates are not a replacement for encrypted full backups. -> Cache compatibility: **Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`) defaults to `true` in 0.34.7, also for configs without the field. Turning it off restores legacy client freshness handling. See [HTTP cache](CACHE.md#varnish-compatible-cache-policy-0347) for semantics and safety exceptions. +> Cache compatibility: **Settings → Cache → Varnish-compatible cache policy** (`cache.varnish_compatible`) defaults to `true` in 0.34.7, also for configs without the field. Turning it off restores legacy client freshness handling. Implicit grace defaults to **60 seconds** (`cache.varnish_grace_seconds`, editable in Settings → Cache). See [HTTP cache](CACHE.md#varnish-compatible-cache-policy-0347) for semantics and safety exceptions. ## Configuration storage: SQLite or JSON file diff --git a/langs/en_US.json b/langs/en_US.json index 5abd1e6..33c7fc4 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -289,8 +289,13 @@ "saveResponse": "Apply response settings", "cacheSettings": "Cache", "enableCache": "Enable cache", + "cacheVarnishGrace": "Default grace (seconds, 0 = off)", "cacheVarnishCompatible": "Varnish-compatible cache policy (recommended; enabled by default)", - "cacheVarnishCompatibleHelp": "On: ignore client Cache-Control/Pragma when looking up cached objects. Shift+F5, no-cache, max-age=0 and only-if-cached cannot force an origin fetch while a fresh HIT exists. Respect origin private/no-store/no-cache and Surrogate-Control no-store; do not cache responses with zero remaining TTL; permit up to 10 s grace for eligible stale entries. Keep RustProxy static-cookie exceptions, Authorization, Vary, explicit TTL, invalidation, transforms and HOT/COLD unchanged. Off: original strict client cache directives. Changing modes invalidates existing cached objects. This is not full Varnish VCL emulation.", + "cacheVarnishCompatibleHelp": "A browser hard reload (Shift+F5, Cache-Control: no-cache or max-age=0, Pragma: no-cache) keeps a fresh cache HIT, including Age and the hit count.", + "cacheVarnishCompatibleInfoLabel": "Varnish-compatible cache behavior", + "cacheVarnishCompatibleSafeguards": "Still honors client no-store, only-if-cached (504 on MISS, no origin access or background refresh), min-fresh and max-stale (including max-stale=0 during grace). Cookie exceptions for static files, Authorization, Vary, PURGE/BAN, HOT/COLD and transforms work as before.", + "cacheVarnishCompatibleBackendHelp": "Origin no-cache with ETag/Last-Modified remains revalidatable. Setting a positive fallback TTL explicitly permits public no-cache responses to be reused for that TTL, even without validators. Origin private/no-store/Set-Cookie and Surrogate-Control: no-store are never overridden. Zero-TTL entries may still provide stale-if-error or validator revalidation.", + "cacheVarnishCompatibleModeHelp": "Grace defaults to 60 seconds and can be changed in the field below. It only applies to eligible fresh-at-storage objects without an origin stale-while-revalidate directive; explicit =0 is honored. Off restores strict client reload semantics. Switching modes invalidates existing cached entries. This is a compatibility subset, not full Varnish VCL.", "cacheSafety": "Complete public-safe GET responses are cached. Authorization, cookies and private/no-store responses still bypass. One byte range can be served from a complete cached object; multipart/unsupported ranges pass through.", "cacheMemory": "HOT allocated budget (MiB)", "cacheObject": "Maximum object (KiB)", diff --git a/langs/pl_pl.json b/langs/pl_pl.json index 39877fe..5646ef0 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -289,8 +289,13 @@ "saveResponse": "Zastosuj ustawienia odpowiedzi", "cacheSettings": "Cache", "enableCache": "Włącz cache", + "cacheVarnishGrace": "Domyślny grace (sekundy, 0 = wył.)", "cacheVarnishCompatible": "Polityka cache zgodna z Varnish (zalecana; domyślnie włączona)", - "cacheVarnishCompatibleHelp": "Włączona: ignoruje Cache-Control/Pragma klienta przy odczycie cache. Shift+F5, no-cache, max-age=0 i only-if-cached nie wymuszają pobrania z backendu, jeśli obiekt jest świeży. Nadal blokuje cache dla private/no-store/no-cache i Surrogate-Control: no-store backendu oraz odpowiedzi bez pozostałego TTL. Dla uprawnionych wygasłych obiektów dopuszcza do 10 s grace z odświeżaniem w tle. Zachowuje wyjątki cookies plików statycznych, Authorization, Vary, limity TTL, unieważnianie, transformacje i HOT/COLD. Wyłączona: dotychczasowa ścisła polityka dyrektyw klienta. Zmiana trybu unieważnia aktualne obiekty cache. To nie jest pełna emulacja VCL Varnish.", + "cacheVarnishCompatibleHelp": "Twarde odświeżenie przeglądarki (Shift+F5, Cache-Control: no-cache lub max-age=0, Pragma: no-cache) nie wymusza odświeżenia świeżego wpisu HIT; Age i licznik trafień pozostają zachowane.", + "cacheVarnishCompatibleInfoLabel": "Zachowanie cache w trybie zgodnym z Varnish", + "cacheVarnishCompatibleSafeguards": "Nadal działają: no-store klienta, only-if-cached (504 przy MISS; bez żądania do backendu, także w tle), min-fresh i max-stale (także max-stale=0 przy grace). Zachowane są wyjątki cookies dla statycznych plików, Authorization, Vary, PURGE/BAN, HOT/COLD i transformacje.", + "cacheVarnishCompatibleBackendHelp": "Odpowiedzi no-cache backendu z ETag/Last-Modified nadal mogą być rewalidowane. Dodatni zapasowy TTL świadomie pozwala cache’ować publiczne odpowiedzi no-cache przez wskazany czas, także bez walidatora. Nie nadpisuje private/no-store/Set-Cookie ani Surrogate-Control: no-store. Wpisy z TTL=0 mogą nadal obsługiwać stale-if-error i rewalidację.", + "cacheVarnishCompatibleModeHelp": "Grace wynosi domyślnie 60 s i można go ustawić w polu poniżej. Dotyczy tylko kwalifikujących się wpisów zapisanych jako świeże, bez dyrektywy stale-while-revalidate; jawne =0 jest respektowane. Wyłączenie przywraca ścisłe odświeżanie przez klienta. Zmiana trybu unieważnia obecne wpisy. Nie jest to pełna emulacja VCL Varnish.", "cacheSafety": "Cache zapisuje kompletne, bezpieczne odpowiedzi GET. Authorization, cookies oraz odpowiedzi private/no-store nadal omijają cache. Pojedynczy zakres bajtów może być obsłużony z pełnego obiektu cache; multipart i nieobsługiwane zakresy przechodzą do originu.", "cacheMemory": "Limit alokowanej pamięci HOT (MiB)", "cacheObject": "Limit obiektu (KiB)", diff --git a/src/admin/openapi.json b/src/admin/openapi.json index a7d1f4c..d0c7c12 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2062,10 +2062,17 @@ }, "default": [] }, + "varnish_grace_seconds": { + "type": "integer", + "minimum": 0, + "maximum": 604800, + "default": 60, + "description": "Implicit grace window in seconds for eligible positive-TTL objects in Varnish-compatible mode without an explicit origin stale-while-revalidate directive. Zero disables implicit grace. Explicit origin stale-while-revalidate, including =0, takes precedence." + }, "varnish_compatible": { "type": "boolean", "default": true, - "description": "Default true: Varnish-style request cache policy (ignore client Cache-Control/Pragma directives including no-cache, max-age=0, only-if-cached); reject origin no-cache, Surrogate-Control no-store and nonpositive remaining TTL; 10-second stale-while-revalidate grace for eligible stored objects. Existing RustProxy privacy, cookies, Vary, explicit TTL and invalidation options remain in force. False restores strict client-cache directive handling; switching modes invalidates cache; not full VCL emulation." + "description": "Default true: ignore only client hard-reload no-cache, max-age=0 and Pragma: no-cache on fresh cache lookups. Preserve client no-store, only-if-cached (504 on MISS), min-fresh/max-stale, origin ETag revalidation and zero-TTL stale-if-error storage. A positive default_ttl_seconds explicitly allows public origin no-cache responses to be reused for that fallback TTL, even without validators; zero keeps validator-only revalidation. Surrogate-Control: no-store and all private/no-store/Set-Cookie/Vary restrictions remain active. Eligible positive-TTL entries without an explicit stale-while-revalidate directive get configurable grace (varnish_grace_seconds, default 60s; explicit =0 is honored). False restores strict browser reload. Switching modes invalidates cache; not full Varnish VCL emulation." } }, "additionalProperties": false diff --git a/src/admin/web/app/settings.js b/src/admin/web/app/settings.js index b7a0c69..ac3bf52 100644 --- a/src/admin/web/app/settings.js +++ b/src/admin/web/app/settings.js @@ -11,7 +11,7 @@ function renderSettings() { $("compress-min").value = compression.min_bytes ?? 1024; syncServerMode(); } if (!settingsDirty.has("cache-form")) { - const c = config.cache || {}; $("cache-enabled").checked = !!c.enabled; $("cache-varnish-compatible").checked = c.varnish_compatible !== false; $("cache-memory").value = (c.max_bytes ?? 268435456) / 1048576; + const c = config.cache || {}; $("cache-enabled").checked = !!c.enabled; $("cache-varnish-compatible").checked = c.varnish_compatible !== false; $("cache-varnish-grace").value = c.varnish_grace_seconds ?? 60; $("cache-memory").value = (c.max_bytes ?? 268435456) / 1048576; $("cache-transient-memory").value = (c.transient_max_bytes ?? 67108864) / 1048576; $("cache-object").value = (c.max_object_bytes ?? 16777216) / 1024; $("cache-entries").value = c.max_entries ?? 4096; $("cache-eviction-limit").value = c.hot_eviction_max_objects ?? 64; @@ -286,7 +286,7 @@ bindSettingsForm("response-form", next => { next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked; }, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression })); bindSettingsForm("cache-form", next => { - next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, varnish_compatible: $("cache-varnish-compatible").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), http_invalidation_allowed_cidrs: $("cache-http-invalidation-cidrs").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, expose_only_hits: $("cache-only-hits").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null }; + next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, varnish_compatible: $("cache-varnish-compatible").checked, varnish_grace_seconds: Number($("cache-varnish-grace").value), memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), http_invalidation_allowed_cidrs: $("cache-http-invalidation-cidrs").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, expose_only_hits: $("cache-only-hits").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null }; }, "cache"); bindSettingsForm("transform-form", next => { next.transform = { diff --git a/src/admin/web/index.html b/src/admin/web/index.html index 9c01854..70ce7f0 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -2210,9 +2210,24 @@ Authorization, cookies and private/no-store responses still bypass the cache. One byte range can be served from a complete cached object; multipart/unsupported ranges pass through.
- -When enabled, client Cache-Control and Pragma are ignored for cache lookup: Shift+F5/no-cache/max-age=0 does not replace a fresh HIT or reset its Age. Response private/no-store/no-cache and Surrogate-Control no-store stay uncacheable, responses with no remaining TTL stay out of cache, and eligible expired objects may use up to 10 seconds of background-refresh grace. Existing static-cookie exceptions, Authorization protection, Vary, explicit TTL limits, BAN/PURGE, HOT/COLD, transforms and unsafe-force rules remain independent. Off restores the previous strict client HTTP caching policy. Changing modes invalidates stored objects. This is not full Varnish VCL emulation.
+A browser Shift+F5 (no-cache, max-age=0 or Pragma: no-cache) uses a fresh cache HIT without replacing it or resetting Age.
+Preserves client no-store, only-if-cached (504 on MISS; no origin contact or background refresh), min-fresh and max-stale (including max-stale=0 during grace); existing cookie rules, Authorization, Vary, PURGE/BAN, HOT/COLD and transformations stay active.
+Origin no-cache with a validator remains revalidatable. If an explicit fallback TTL is set, public no-cache responses may instead be reused for that TTL, even without a validator. Private/no-store/Set-Cookie and Surrogate-Control: no-store remain blocked. Zero-TTL objects may be retained for stale-if-error or revalidation; explicit stale-while-revalidate=0 is respected.
+Implicit grace (60 seconds by default, configurable below) only applies to eligible fresh-at-storage responses without an explicit stale-while-revalidate directive. Off restores strict client reload semantics. Changing the mode invalidates existing cached objects. This is not full Varnish VCL emulation.
+