Skip to content

[HttpKernel] ESI fragment content may be missing in conditional requests #58015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 16, 2024

Conversation

mpdude
Copy link
Contributor

@mpdude mpdude commented Aug 15, 2024

Q A
Branch? 6.4
Bug fix? yes
New feature? no
Deprecations? no
Issues
License MIT

The content for ESI-embedded fragments may be missing from the combined (main) response generated by the HttpCache under certain conditions:

  1. The main request sent to the cache must be a conditional request with either If-Modified-Since or If-None-Match
  2. The embedded response processed by the cache matches the validator (last-modified or etag)
  3. The resulting (combined) response generated by the HttpCache does not match the validator

Condition 3 is necessary since otherwise the cache returns an empty 304 response. In that case, the issue still exists, but we don't see or care about it and the wrong body is not sent at all.

Regarding condition 2, it does not matter where this embedded response comes from. It may be a fresh (cached) response taken from the cache, it may be a stale cache entry that has been revalidated; probably it can even be a non-cacheable response.

In practice, the conditional request will always use If-Modified-Since: We're dealing with ESI subrequests, and the combined response created by the HttpCache does not provide ETags, so a client has nothing to validate against.

Only since #42355 (merged in to 6.2) the main response will include a Last-Modified header, given that all of the included responses provided one. Probably that is the reason why this bug was not spotted earlier - it required that change and all of the responses processed by the cache must provide Last-Modified data.

Conditions 2 + 3 together seem unlikely, but may in fact happen easily when you have an application that generates different chunks of cacheable content for the main and embedded requests and adds last-modified information to them. For example:

  • First request: Main response modified at time 1, embedded fragment modified at time 2 -> last-modified at 2
  • Data for main response changes at time 3
  • Second request with "If-Modified-Since" time 2
  • Embedded fragment still modified at time 2, main response at time 3 -> last-modified at 3
    • the embedded fragment is considered as not modified, content is stripped
    • main response is generated, fragment content is missing

@mpdude
Copy link
Contributor Author

mpdude commented Aug 15, 2024

Asking @aschempp and @Toflar for reviews, since I know you guys are familiar with the HTTP cache details

@mpdude
Copy link
Contributor Author

mpdude commented Aug 15, 2024

Remaining test failures have to do with Twig and seem unrelated

Copy link
Contributor

@aschempp aschempp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to step through the HttpCache to validate this, because I was unsure if ESI is actually a subrequest. I think I can confirm this works, with a side not though.

Unfortunately, the HttpCache does currently not store the final response in the cache. Which means to validate Last-Modified, we have to fetch the main content and all ESI contents including their headers from the cache, then reuse the ResponseCacheStragey on all of these responses to check if Last-Modified is still the same. If we had the final content cached, we could skip this whole process. Caching the final response only makes sense if there is a validation (Last-Modified or maybe Etag) in it. And it is certainly out of the scope of this PR 😅


Brain dump™ on the process:

  1. HttpCache::handle receives a request and tries to load it from cache or backend (dependin on whether the method is safe etc.)
  2. then replaces ESI (HttpCache::restoreResponseBody) on the content
  3. then updates the ResponseCacheStrategy, add or update depending on the request type (main/sub)
  4. then validates if the Last-Modified header matches and might throw away the content

This process is the same for the main request and sub request. However, a sub request must not throw away its content, because it must be merged into the main request. Because only after mergin all subrequest, the ResponseCacheStrategy can determine if the content is needed or can be thrown away.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just use assertSame where possible instead of assertEquals

@mpdude
Copy link
Contributor Author

mpdude commented Aug 16, 2024

@nicolas-grekas updated

@nicolas-grekas nicolas-grekas force-pushed the esi-revalidation-lost-content branch from 4090d5b to 9fed8dc Compare August 16, 2024 09:49
@nicolas-grekas
Copy link
Member

Thank you @mpdude.

@nicolas-grekas nicolas-grekas merged commit e1bbaf2 into symfony:6.4 Aug 16, 2024
5 of 7 checks passed
@mpdude mpdude deleted the esi-revalidation-lost-content branch August 16, 2024 09:58
This was referenced Aug 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants