litespeedtech / litespeedtech/openlitespeed

feat(cache): honor If-Modified-Since on public LSCache HIT (304 without ETag)

Open
#497 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.