Skip to content

fix(dav): make contact photo cache max-age configurable#59551

Open
arndttouby wants to merge 6 commits intonextcloud:masterfrom
arndttouby:fix/dav-photo-cache-control
Open

fix(dav): make contact photo cache max-age configurable#59551
arndttouby wants to merge 6 commits intonextcloud:masterfrom
arndttouby:fix/dav-photo-cache-control

Conversation

@arndttouby
Copy link
Copy Markdown

@arndttouby arndttouby commented Apr 9, 2026

Summary

Make the Cache-Control: max-age value for contact photo exports configurable via the app config key dav.contact_photo_cache_max_age (default: 3600).

This addresses the concern raised in the initial review: removing caching for 204 No Content responses would cause unnecessary database lookups on large installations. Instead, the default behavior is preserved (1 hour cache), but administrators can tune the value as needed — including setting it to 0 for development or debugging.

Changes

  • ImageExportPlugin.php: Inject IConfig, read contact_photo_cache_max_age app value (default 3600), use it in the Cache-Control header instead of a hardcoded value.
  • Server.php: Pass IConfig as second argument to ImageExportPlugin constructor.
  • ImageExportPluginTest.php: Add IConfig mock to existing tests, add two new test cases (testCardWithCustomMaxAge with 120s, testCardWithZeroMaxAge with 0s).

Usage

# Set a custom max-age (e.g. 2 minutes)
occ config:app:set dav contact_photo_cache_max_age --value=120

# Disable caching entirely
occ config:app:set dav contact_photo_cache_max_age --value=0

# Reset to default (1 hour)
occ config:app:delete dav contact_photo_cache_max_age

Checklist

  • Code changes
  • Unit tests added
  • Default behavior unchanged (max-age=3600)
  • No performance regression (no additional DB queries by default)

The ImageExportPlugin sets Cache-Control: max-age=3600 before the
try/catch block, which means 204 No Content responses (no photo found)
are also cached by the browser for 1 hour. If a photo is added to a
contact after the initial request, the browser serves the cached empty
response and the photo remains invisible until the cache expires.

Move Cache-Control and ETag headers inside the try block so they are
only set for 200 OK responses (photo found). For 204 responses, set
Cache-Control: no-cache, no-store, must-revalidate so the browser
always re-checks on the next request.

Signed-off-by: Arndt Touby <arndt@touby.eu>
@SebastianKrupinski
Copy link
Copy Markdown
Contributor

Hi @arndttouby

Thank you for taking the time to create this PR.

@SebastianKrupinski
Copy link
Copy Markdown
Contributor

So the code looks good.

But there is a down side to this change and probably why the code was originally written the way it was.

Yes the changes you made address one specific scenario, when a user adds a photo, the photo might not show for an hour.

But the changes now causes the client to continuously make requests for photos that don't exist every time the page loads, this would cause 1000's of unneeded requests on some systems, that access the database to check if there is a photo.

I think the trade off, of the one hour delay vs the performance impact of the changes you are suggesting is worth it.

Replace hardcoded max-age=3600 with a configurable app value
'contact_photo_cache_max_age' (default: 3600).

Signed-off-by: Arndt Touby <arndt@touby.eu>
@arndttouby arndttouby requested a review from a team as a code owner April 10, 2026 19:09
@arndttouby arndttouby requested review from Altahrim, ArtificialOwl, come-nc and icewind1991 and removed request for a team April 10, 2026 19:09
Replace hardcoded max-age=3600 with a configurable app value
'contact_photo_cache_max_age' (default: 3600). Add IConfig injection
and unit tests for custom and zero max-age values.

Signed-off-by: Arndt Touby <arndt@touby.eu>
@arndttouby arndttouby changed the title fix(dav): set proper Cache-Control for 204 No Content in ImageExportPlugin fix(dav): make contact photo cache max-age configurable Apr 10, 2026
@arndttouby
Copy link
Copy Markdown
Author

Hi @SebastianKrupinski, thanks for the thorough review — your concern about the performance impact is absolutely valid.

I've reworked the approach based on your feedback. Instead of moving the Cache-Control header inside the try/catch (which would remove caching for 204 responses and cause unnecessary DB lookups), the new implementation makes max-age configurable via an app config value:

$maxAge = $this->config->getAppValue('dav', 'contact_photo_cache_max_age', '3600');

Key points:

  • Default behavior is unchangedmax-age=3600 (1 hour) remains the default, so there is no performance regression on existing installations.
  • Administrators can tune it — e.g. occ config:app:set dav contact_photo_cache_max_age --value=120 for a shorter cache, or --value=0 to disable caching entirely for debugging.
  • IConfig is injected via the constructor (already available in Server.php).
  • I've also added unit tests for custom max-age (120s) and zero max-age (0s) scenarios.

This way, the one-hour default stays in place for large installations, but smaller setups or admins who need faster photo updates have a knob to turn.

…r.php

Signed-off-by: Arndt Touby <arndt@touby.eu>
Ensures the Cache-Control max-age value is always numeric,
preventing potential header injection via malformed config values.

Signed-off-by: Arndt Touby <arndt@touby.eu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants