litespeedtech / litespeedtech/openlitespeed
feat(cache): honor If-Modified-Since on public LSCache HIT (304 without ETag)
- Dominant language
- C++
- Stars
- 1.5k
- Forks
- 233
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 5
Description
## Summary
Please support **HTTP 304 Not Modified** for **public LSCache HIT** when the client sends only `If-Modified-Since` matching the stored `Last-Modified`, even if `If-None-Match` / ETag is absent.
Today (verified on **LiteSpeed Web Server Enterprise 6.3.5**, same LSCache engine family), a public cache HIT returns:
| Conditional request | Result |
|---|---|
| `If-None-Match` matching stored strong ETag | **304**, 0 body |
| `If-Modified-Since` matching stored `Last-Modified` (no INM) | **200**, full HTML body |
Static file serving already uses mtime / ETag-style validation; the gap is specifically the **dynamic public cache object** serve path.
## Why this matters
Many reverse proxies and CDNs (notably **Cloudflare** on non-Enterprise plans) **strip or weaken origin `ETag` on `text/html`** whenever any edge feature rewrites the body (Fonts, Web Analytics, Email Obfuscation, JS Detections, Rocket Loader, etc.). That is HTTP-correct once the body is transformed.
`Last-Modified` often **still reaches the client**. Crawlers such as **Googlebot** support both validators ([Google: HTTP caching](https://developers.google.com/search/blog/2024/12/crawling-december-caching)): if ETag is gone they fall back to `If-Modified-Since`.
With current LSCache HIT behavior, those crawlers keep downloading the full HTML on every recrawl even though the cached object is unchanged — wasting origin bandwidth and crawl budget. There is no LSCache / module / `httpd_config` knob to enable IMS-on-HIT (PrestaShop `litespeedcache` only emits `X-LiteSpeed-Cache-Control` / tags; validation lives in the server binary).
## Environment (repro)
- Server: LiteSpeed Web Server **6.3.5** (Enterprise), LSCache public cache enabled
- App: PrestaShop + LiteSpeed Cache module (headers only; origin also emits `ETag` + `Last-Modified` via a small FO module)
- Edge: Cloudflare in front (ETag stripped on HTML; `Last-Modified` preserved)
### Repro (origin, cookieless, after public HIT warm)
```bash
UA='Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
U='https://example.com/' # any public-cacheable URL with X-LiteSpeed-Cache: hit
# 1) Capture validators from HIT
curl -s -A "$UA" -H 'Cookie:' -D h -o /dev/null "$U"
ETAG=$(awk 'tolower($1)=="etag:"{print $2}' h | tr -d '\r')
LM=$(sed -n 's/^[Ll]ast-[Mm]odified: //p' h | tr -d '\r')
# 2) INM → 304
curl -s -A "$UA" -H 'Cookie:' -H "If-None-Match: $ETAG" -o /dev/null -w '%{http_code} %{size_download}\n' "$U"
# expect: 304 0
# 3) IMS alone → 200 (full body) ← requested change: 304 0
curl -s -A "$UA" -H 'Cookie:' -H "If-Modified-Since: $LM" -o /dev/null -w '%{http_code} %{size_download}\n' "$U"
# currently: 200
```
## Proposed behavior
On **public** (and ideally private) LSCache HIT:
1. If `If-None-Match` is present → keep current ETag logic (strong/weak as today).
2. Else if `If-Modified-Since` is present and compares equal (or not later than) the `Last-Modified` stored with the cache object → respond **304** with empty body, same as INM match.
3. Optional config flag (default off or on — either is fine) under Cache / tuning if you prefer opt-in.
RFC 9110 / HTTP caching: either validator is sufficient for conditional GET; preferring ETag when both are present is fine.
## Scope note
This is a **server cache-engine** change (`lshttpd` / OLS cache), not something the PrestaShop/WordPress LSCache plugins can implement: on HIT, PHP never runs.
Happy to provide more traces (response headers, `X-LiteSpeed-Cache: hit`, sample `Last-Modified`/`ETag` pairs) if useful.
Thanks for considering this — it would make 304 crawl savings work in front of CDNs that drop ETags but keep `Last-Modified`.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the public-cache HIT path in the lshttpd/OpenLiteSpeed cache engine; the issue identifies this as the dynamic public cache object serve path. Reproduce the current behavior with the provided curl commands, then verify that an IMS-only request matching the stored Last-Modified returns 304 with an empty body while existing ETag behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- backend, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100