Skip to content

fix: openproject avatar remain stale due to long browser cache#1058

Open
Ashim-Stha wants to merge 1 commit into
release/3.1from
fix-avatar-cache
Open

fix: openproject avatar remain stale due to long browser cache#1058
Ashim-Stha wants to merge 1 commit into
release/3.1from
fix-avatar-cache

Conversation

@Ashim-Stha

@Ashim-Stha Ashim-Stha commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Description

  • Replaced max-age=86400 with Cache-Control: no-cache so the browser always revalidates the avatar on each request
  • Added ETag support based on md5 of the avatar content so the browser can skip downloading the body if the avatar hasn't changed (304 Not Modified)

References: see ref1, ref2

Related Issue or Workpackage

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Updated CHANGELOG.md file

@Ashim-Stha Ashim-Stha self-assigned this Jun 15, 2026
@Ashim-Stha Ashim-Stha force-pushed the fix-avatar-cache branch 2 times, most recently from e075b26 to 0d713f7 Compare June 15, 2026 10:36
Comment thread lib/Controller/OpenProjectAPIController.php Outdated
@Ashim-Stha Ashim-Stha force-pushed the fix-avatar-cache branch 4 times, most recently from d22098b to 32236ec Compare June 16, 2026 08:53
Comment thread lib/Controller/OpenProjectAPIController.php Outdated
@Ashim-Stha Ashim-Stha force-pushed the fix-avatar-cache branch 10 times, most recently from b3af3f3 to db32125 Compare June 16, 2026 11:48
@Ashim-Stha Ashim-Stha requested a review from saw-jan June 16, 2026 11:48
Comment thread lib/Controller/OpenProjectAPIController.php Outdated
Comment thread lib/Service/OpenProjectAPIService.php Outdated
Comment thread lib/Service/OpenProjectAPIService.php Outdated
@Ashim-Stha Ashim-Stha force-pushed the fix-avatar-cache branch 3 times, most recently from 299ef37 to bbda2c6 Compare June 17, 2026 07:19
Signed-off-by: Ashim Shrestha <ashimshrestha2384@gmail.com>
@github-actions

Copy link
Copy Markdown

JS Code Coverage

Coverage after merging fix-avatar-cache into release/3.1 will be
92.37%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   adminSettings.js0%0%0%0%1, 1, 10–19, 2–9
   bootstrap.js0%0%0%0%1, 1, 10–12, 2–9
   dashboard.js0%0%0%0%1, 1, 10–19, 2–9
   personalSettings.js0%0%0%0%1, 1, 10–19, 2–9
   projectTab.js0%0%0%0%1, 1, 10–19, 2, 20–29, 3, 30–37, 4–9
   reference.js0%0%0%0%1, 1, 10–19, 2, 20–29, 3, 30–39, 4, 40–45, 5–9
   utils.js85.92%33.33%50%87.59%12–20, 23–32
src/api
   endpoints.js100%100%100%100%
   settings.js64.71%100%0%73.33%10–11, 14–15
src/components
   AdminSettings.vue96.06%94.74%97.10%96.19%1003–1005, 1025–1028, 568, 568–569, 569, 606–612, 715–716, 720–721, 724–725, 729–730, 740–745, 788–790, 802–805, 818–820, 842–844, 927–929, 964–967
   ErrorLabel.vue100%100%100%100%
   OAuthConnectButton.vue85.82%63.64%100%87.39%49–56, 64–69, 72–76
   PersonalSettings.vue92.02%95.65%90%91.71%133–134, 144–149, 152–161
src/components/admin
   FieldValue.vue100%100%100%100%
   FormAuthMethod.vue98.12%96.88%100%98.12%222–224, 247–250
   FormHeading.vue100%100%100%100%
   FormOpenProjectHost.vue98.87%94.74%100%99.34%167–169, 279
   FormSSOSettings.vue98.57%97.18%96.55%98.91%231–233, 242–243, 358–359
   TermsOfServiceUnsigned.vue100%100%100%100%
   TextInput.vue100%100%100%100%
src/components/icons
   ClippyIcon.vue100%100%100%100%
   OpenProjectIcon.vue100%100%100%100%
src/components/settings
   CheckBox.vue100%100%100%100%
   ErrorNote.vue100%100%100%100%
   SettingsTitle.vue96.91%85.71%100%97.67%51–53
src/components/tab
   EmptyContent.vue97.98%90.91%100%98.82%102–103, 107–108
   SearchInput.vue95.31%92.96%94.74%95.78%138–139, 192, 203–208, 267–269, 285–287, 291–296
   WorkPackage.vue86.25%73.17%93.33%87.62%107–116, 129–131, 142–146, 156–158, 176–182, 220, 220–225, 225, 225–236, 81–82
src/constants
   appID.js100%100%100%100%
   links.js100%100%100%100%
   messages.js100%100%100%100%
src/filesPlugin
   filesPlugin.js0%0%0%0%1, 1, 10, 100–109, 11, 110–113, 12–19, 2, 20–29, 3, 30–39, 4, 40–49, 5, 50–59, 6, 60–69, 7, 70–79, 8, 80–89, 9, 90–99
src/utils
   workpackageHelper.js93.80%93.10%88.89%94.24%100–102, 23–27, 54, 54–56, 97–99
src/views
   CreateWorkPackageModal.vue94.14%86.32%90.48%95.40%366–371, 374, 390, 507–510, 515–520, 525–530, 536–539, 542, 558, 558, 599–603, 613–615, 638–639, 647–649, 678–680, 702–704, 713–717
   Dashboard.vue93.18%94.20%82.61%93.75%119–124, 133, 143, 146, 157–159, 213–216, 219–220, 227–231
   LinkMultipleFilesModal.vue99.14%97.56%100%99.32%157–159
   ProjectsTab.vue94.95%94.34%93.75%95.10%104–107, 143, 154–155, 189–199, 248–250

@Ashim-Stha Ashim-Stha marked this pull request as ready for review June 17, 2026 08:31
@github-actions

Copy link
Copy Markdown

PHP Code Coverage

Coverage after merging fix-avatar-cache into release/3.1 will be
67.34%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
integration_openproject/server/apps/integration_openproject/lib
   Capabilities.php0%100%0%0%24, 31–40
   OIDCClientMapper.php0%100%0%0%29, 40–49, 52
   ServerVersion.php0%100%0%0%17, 27–29, 32, 41–42
   TokenEventFactory.php100%100%100%100%
integration_openproject/server/apps/integration_openproject/lib/AppInfo
   Application.php36.59%100%50%35.53%105–107, 110–114, 116–121, 123–124, 127, 132–133, 135–138, 140, 142, 144, 148–151, 153–164, 166, 169, 173, 177–179, 212
integration_openproject/server/apps/integration_openproject/lib/BackgroundJob
   RemoveExpiredDirectUploadTokens.php0%100%0%0%28, 30–32, 41–42
integration_openproject/server/apps/integration_openproject/lib/Controller
   ConfigController.php75.12%100%64.71%75.56%114, 151–152, 154, 156–158, 160–163, 166–167, 169, 194, 198–200, 442–444, 446–448, 497, 541–543, 577–581, 592, 606–609, 617, 621–624, 660, 663–678, 695–700, 702–703, 705–707, 710, 712–728, 742–745, 747–751
   DirectDownloadController.php0%100%0%0%33–35, 50–52, 54–61
   DirectUploadController.php71.03%100%100%70.21%117–119, 162–164, 175, 179–182, 184, 194, 201, 217–219, 221–222, 225–230, 233, 235, 245–247, 253–255, 263–265, 280–282, 301, 306, 312
   FilesController.php85.60%100%83.33%85.71%181–182, 244, 253, 270–273, 278–280, 285–287, 299, 301, 304
   OpenProjectAPIController.php81.09%100%82.35%80.98%116, 155, 202–204, 207–214, 216–220, 222, 241, 266, 331, 381, 401, 448, 473–475, 478–482, 484, 66
   OpenProjectController.php96.45%100%80%96.95%241–245
integration_openproject/server/apps/integration_openproject/lib/Dashboard
   OpenProjectWidget.php0%100%0%0%101, 108–109, 111–116, 118–122, 124–126, 129–140, 61–66, 73, 80, 87, 94
integration_openproject/server/apps/integration_openproject/lib/Exception
   OpenprojectAvatarErrorException.php100%100%100%100%
   OpenprojectErrorException.php100%100%100%100%
   OpenprojectFileNotUploadedException.php100%100%100%100%
   OpenprojectGroupfolderSetupConflictException.php100%100%100%100%
   OpenprojectResponseException.php100%100%100%100%
   OpenprojectUnauthorizedUserException.php0%100%0%0%21
integration_openproject/server/apps/integration_openproject/lib/Listener
   BeforeGroupDeletedListener.php100%100%100%100%
   BeforeNodeInsideOpenProjectGroupfilderChangedListener.php0%100%0%0%46–48, 52–55, 57, 59, 62–63, 65, 67–70, 72–75, 77–79
   BeforeUserDeletedListener.php0%100%0%0%30, 37–38, 40–43, 45
   LoadAdditionalScriptsListener.php0%100%0%0%37–38, 46–47, 49, 51–52, 54
   LoadSidebarScript.php65.91%100%100%64.29%105, 77–88, 95, 98
   OpenProjectReferenceListener.php0%100%0%0%45–47, 54–55, 57, 59–60, 62–74
   TermsOfServiceEventListener.php0%100%0%0%41–42, 47–48, 50–51, 53–55, 58–62
   UserChangedListener.php0%100%0%0%34, 41–42, 45–50, 53
integration_openproject/server/apps/integration_openproject/lib/Migration
   Version2001Date20221213083550.php0%100%0%0%30, 40–48, 50–58, 60–62, 64
   Version20100Date20250820101358.php0%100%0%0%38, 47–53, 56
   Version2310Date20230116153411.php0%100%0%0%29, 32–35, 37–62, 64–65, 67
   Version2400Date20230504144300.php0%100%0%0%30, 40–43
   Version2640Date20240628114301.php0%100%0%0%35, 47–49, 52–53, 56
   Version2900Date20250718065820.php0%100%0%0%22, 33–36, 38–39, 41–42, 45
integration_openproject/server/apps/integration_openproject/lib/Reference
   WorkPackageReferenceProvider.php51.67%100%25%58.33%100–101, 104, 108, 142, 150–151, 159, 37, 44, 51, 58–60, 87, 93–96, 99
integration_openproject/server/apps/integration_openproject/lib/Search
   OpenProjectSearchProvider.php0%100%0%0%100–102, 104–106, 109–110, 112–113, 116–125, 127–131, 52–55, 62, 69, 77, 79, 82, 89–90, 93–97, 99
   OpenProjectSearchResultEntry.php100%100%100%100%
integration_openproject/server/apps/integration_openproject/lib/Service
   DatabaseService.php42.31%100%60%40.43%108–112, 114, 63–76, 78–85
   DirectDownloadService.php88.46%100%100%87.50%62–63, 65
   DirectUploadService.php42.86%100%66.67%40%101, 62–65, 67–75, 95
   OauthService.php0%100%0%0%100–101, 40–42, 51–58,

'Cache-Control' => 'no-cache',
'ETag' => $etag,
];
$ifNoneMatch = $this->request->getHeader('If-None-Match');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
$ifNoneMatch = $this->request->getHeader('If-None-Match');
$ifNoneMatch = $this->request->getHeader('If-None-Match') ?? '';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

request->getHeader will return empty string if not found

);
public function getOpenProjectAvatarDataProvider(): array {
return [
'oauth - 200 OK' => [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
'oauth - 200 OK' => [
'OAuth: returns 200 OK when If-None-Match differs from ETag' => [

'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_OK,
],
'oauth - 304 Not Modified' => [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
'oauth - 304 Not Modified' => [
'OAuth: returns 304 Not Modified when If-None-Match matches ETag' => [

'ifNoneMatch' => '"some etag"',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_NOT_MODIFIED,
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it's good to check ifNoneMatch if it's empty.

Comment on lines +402 to +407
$avatarFile = $avatar->getFile(64);
return [
'avatar' => $avatarFile->getContent(),
'type' => $avatarFile->getMimeType(),
'etag' => md5($avatarFile->getContent()),
];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
$avatarFile = $avatar->getFile(64);
return [
'avatar' => $avatarFile->getContent(),
'type' => $avatarFile->getMimeType(),
'etag' => md5($avatarFile->getContent()),
];
$avatarFile = $avatar->getFile(64);
$avatarContent = $avatarFile->getContent();
return [
'avatar' => $avatarContent,
'type' => $avatarFile->getMimeType(),
'etag' => md5($avatarContent),
];

$etag = '"' . $etag . '"';
$this->assertSame(
"no-cache",
$response->getHeaders()["Cache-Control"]

@nabim777 nabim777 Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

getHeaders() is repeated. You can make simply

$headers = $response->getHeaders()

and use this in multiple places.

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.

3 participants