You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Screenshot mirror returns an empty body for a quarter of all apps #26
apps.json hands clients raw screenshot URLs; a Nextcloud instance never fetches those
itself but asks the mirror at https://usercontent.apps.nextcloud.com/<base64 of the URL as it appears in apps.json>.
For a large share of apps that mirror answers HTTP 200 with an empty body, while the
source URL serves the image fine. Instances therefore show no screenshot in the app list.
The store website is unaffected — it embeds the source URL directly.
The base64 key must be built from the URL string exactly as it appears in apps.json,
including any surrounding whitespace — several apps write <screenshot> across multiple
lines in info.xml, and that whitespace is part of the key. (Whitespace itself is not the
bug: 80 of the 88 whitespace-carrying URLs are mirrored correctly.)
Expected behaviour
The mirror serves the image, as it does for the majority of apps.
Actual behaviour
Measured 2026-09-09 against /api/v1/platform/34.0.0/apps.json:
result
screenshot URLs
image delivered
763
empty body (0 bytes)
318
body File not found (14 bytes)
5
total
1086 (from 381 apps)
97 of 381 apps with screenshots have at least one empty image.
Examples: moviedb, crate, n8n_sync, homecheck, maintenancecheck, twofactor_email, folder_protection, kanso, integration_pexip, agora, llmchat, launchpad.
A random sample of ten affected URLs was fetched from their source: all ten returned
HTTP 200 with the full image (94–630 KB). So the source is not the problem.
Guess
A first fetch that failed appears to be cached as an empty entry and never retried.
Re-fetching on an empty cache entry, or not caching failures at all, would fix it.
Thanks for letting us know! First of all, this does indeed concern the usercontent.apps.nextcloud.com project rather than the app store itself, so I'm moving this issue over there. Second, I'm not sure if there's actually an issue with the current code, but some images may have slipped through the cracks due to recent changes to the project. Either way, it might be useful to add a check on the existing images and delete those with an empty or File not found body. If you're interested in working on this, we happily accept and review PRs.
Thanks for looking into it. Sorry for the wrong repo. I don't have time to work on it, found it while debugging my own app twofactor_email, and I'm quite busy with it. Not sure whether I should close here.
Data that may help, from extended_occweb (release uploaded 2026-10-01 01:11 UTC). I can't tell whether #31 is deployed yet, so this only describes what the mirror returns.
Until this release these keys answered File not found; since the release they answer with an empty body, so something now writes under the key the store hands out (which the double-encoding bug fixed in #31 would not do). They were still empty at 17:30 UTC, more than 16 hours after the release and after at least two of the 6-hour retry windows (RETRY_BACKOFF_S).
Other apps with a recent release show the same split (first screenshot of each, checked 2026-10-01 12:05 UTC; the ones that work may have been cached under the same URL before):
Newest release (UTC)
App
Source host
Mirror
2026-10-01 11:13
atrium_secureshare
raw.githubusercontent.com
empty
2026-10-01 07:54
files_archive
github.com/…/raw
image
2026-10-01 05:33
kanso
raw.githubusercontent.com
empty
2026-10-01 03:12
ncc_backend_4mc
raw.githubusercontent.com
image
2026-10-01 01:11
extended_occweb
github.com/…/raw
empty
2026-10-01 00:17
file_checksum_search
gitlab.com
empty
2026-09-30 22:37
google_synchronization
github.com/…/raw
image
So it does not depend on the host or on the redirect from github.com/…/raw.
Two things in the code that may explain the empty bodies:
An empty cache file means a failed fetch whose warning image could not be generated: generateWarningImage() writes '' when imagecreatetruecolor() returns false, for example when GD is not available to the sync process. If that is the case on the server, every failed fetch ends up as an empty body instead of the "Preview not available" image, which would also explain the quarter of empty screenshots measured above.
index.php sends Expires: Sun, 17 Jan 2038 for every file it serves, including empty ones. Once a fetch succeeds on a later retry, browsers and caches that already stored the empty response may keep showing nothing.
That still leaves the question why these fetches fail while the source URLs serve the images fine; the sync output for one of the URLs above would probably show it.
A quick deploy of a fix would be much appreciated. A user who opens an app page and sees no screenshots will first blame the developer for careless release work; that many apps are affected only becomes clear when you look around more widely. That reflects badly on developers who build and maintain apps in their own unpaid time, with their name attached to them, while the cause is elsewhere. The least we can hope for is that our reputation doesn't suffer from it. Thank you for looking into it, and thanks again to @martin-rueegg for #31.
I ran into this with my own apps and dug into it. I think the empty bodies come from file permissions in the cache, not from failed fetches.
Survey (2026-10-04). I fetched every screenshot listed in /api/v1/apps.json (1652 URLs) through the mirror and from its source:
491 come back as 200 with an empty body, across 150 apps.
For all 491, the source was a valid image when fetched. 471 still serve one today (max 1.8 MB, median 160 KB, all under MAX_SCREENSHOT_SIZE); the other 20 now return 404.
Source host, format, dimensions and whitespace in the URL make no difference.
The 9 fetches that really failed (source 404, HTML page, unreachable host) are served fine, as the 640×360 "Preview not available" PNG.
When it started. Grouping apps by the week of their first release, which is roughly when their screenshot URLs first reached sync.php:
first release
served
empty
2026-W27 – W29 (Jun 29 – Jul 19)
53
2
2026-W30 (Jul 20 – 26)
16
35
2026-W31 – W40 (Jul 27 – Oct 4)
2
318
So essentially every screenshot first synced after about Jul 21 is empty. Older ones are fine, apart from URLs that only appeared in a later release (my folder_protection has 3/5 working: the two empty ones first shipped in a release on Aug 2).
Likely cause. An empty 200 means realpath() found the cache file, but file_get_contents() returned false. Since #20, a successful fetch is written with tempnam() and then rename()d into cache/. tempnam() creates the file with mode 0600, and rename() keeps that mode. If sync.php runs as a different user than the web server, which may have changed with the redeploy for #23/#24 around Jul 21, index.php can't read the file and echoes nothing. Warning images are written straight into cache/ by imagepng() with the default umask, which is why failures display and successes don't.
I can't see the server, so this is inferred. It fits every data point above, and one ls -l can confirm it, e.g. for one of mine:
ls -l cache/aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2tyZW90cm9waWMvZ3JvdXBfbWFuYWdlci92MC40LjEvc2NyZWVuc2hvdHMvMS1tZW1iZXJzLnBuZw==
I'd expect -rw-------, owned by the user cron runs sync.php as.
Same here for photosphere_tours (release 0.4.11, uploaded 2026-10-02 19:19 UTC, after #31 was merged). All four screenshots return 200 with an empty body from the mirror, while the sources serve fine (e.g. https://raw.githubusercontent.com/weltmaister/photosphere_tours/master/screenshots/viewer.jpg → 200, image/jpeg, 52 KB). Could you trigger a refetch for this app? Thanks!
Yes, feel free to open a PR if you like! We'll review/merge it as soon as we can. Also, perhaps it would be nice to include in sync.php a way to check the permissions for existing cache entries and auto-update them to 0644 if needed (rather than having to manually update the entries ourselves).
Could you trigger a refetch for this app? Thanks!
Unfortunately, refetches are not possible in general with the way the cache currently works. A workaround would be to also change the URLs for any screenshots you wanna update.
Another affected app: nc_gantt (Gantt Projects), release 2.3.0, uploaded 2026-09-30 11:31 UTC, i.e. after #31 was merged. This is the same app as in #29, which was closed after #31.
This matches the tempnam() + rename() → mode 0600 explanation from the comment above, so #32 should fix it. Re-uploading doesn't help, since the cache is only filled once per URL.
Checked on 2026-10-06 against the current apps.json. Happy to re-test once #32 is deployed.
Shortly after the upload the mirror answered File not found (not synced yet); at 20:58 UTC, 1h20 after #32 was merged, all three answer an empty 200. So either #32 isn't deployed yet or sync.php hasn't run since. As ensureReadable() also repairs existing entries, these three should start working on the first sync after deployment without a new upload, which makes them a simple check that the deploy worked. I'll re-test then.
Activity
edward-ly commentedon Sep 9, 2026
Thanks for letting us know! First of all, this does indeed concern the
usercontent.apps.nextcloud.comproject rather than the app store itself, so I'm moving this issue over there. Second, I'm not sure if there's actually an issue with the current code, but some images may have slipped through the cracks due to recent changes to the project. Either way, it might be useful to add a check on the existing images and delete those with an empty orFile not foundbody. If you're interested in working on this, we happily accept and review PRs.nursoda commentedon Sep 10, 2026
Thanks for looking into it. Sorry for the wrong repo. I don't have time to work on it, found it while debugging my own app twofactor_email, and I'm quite busy with it. Not sure whether I should close here.
dillardblom commentedon Oct 1, 2026
Data that may help, from extended_occweb (release uploaded 2026-10-01 01:11 UTC). I can't tell whether #31 is deployed yet, so this only describes what the mirror returns.
apps.json)https://github.com/dillardblom/occweb/raw/main/appinfo/screenshot.pnghttps://github.com/dillardblom/occweb/raw/main/appinfo/screenshot-status.pnghttps://github.com/dillardblom/occweb/raw/main/appinfo/screenshot-sql-switch.pnghttps://github.com/dillardblom/occweb/raw/main/appinfo/screenshot-sql.pngUntil this release these keys answered
File not found; since the release they answer with an empty body, so something now writes under the key the store hands out (which the double-encoding bug fixed in #31 would not do). They were still empty at 17:30 UTC, more than 16 hours after the release and after at least two of the 6-hour retry windows (RETRY_BACKOFF_S).Other apps with a recent release show the same split (first screenshot of each, checked 2026-10-01 12:05 UTC; the ones that work may have been cached under the same URL before):
So it does not depend on the host or on the redirect from
github.com/…/raw.Two things in the code that may explain the empty bodies:
generateWarningImage()writes''whenimagecreatetruecolor()returns false, for example when GD is not available to the sync process. If that is the case on the server, every failed fetch ends up as an empty body instead of the "Preview not available" image, which would also explain the quarter of empty screenshots measured above.index.phpsendsExpires: Sun, 17 Jan 2038for every file it serves, including empty ones. Once a fetch succeeds on a later retry, browsers and caches that already stored the empty response may keep showing nothing.That still leaves the question why these fetches fail while the source URLs serve the images fine; the sync output for one of the URLs above would probably show it.
A quick deploy of a fix would be much appreciated. A user who opens an app page and sees no screenshots will first blame the developer for careless release work; that many apps are affected only becomes clear when you look around more widely. That reflects badly on developers who build and maintain apps in their own unpaid time, with their name attached to them, while the cause is elsewhere. The least we can hope for is that our reputation doesn't suffer from it. Thank you for looking into it, and thanks again to @martin-rueegg for #31.
kreotropic commentedon Oct 4, 2026
I ran into this with my own apps and dug into it. I think the empty bodies come from file permissions in the cache, not from failed fetches.
Survey (2026-10-04). I fetched every screenshot listed in
/api/v1/apps.json(1652 URLs) through the mirror and from its source:200with an empty body, across 150 apps.MAX_SCREENSHOT_SIZE); the other 20 now return 404.When it started. Grouping apps by the week of their first release, which is roughly when their screenshot URLs first reached
sync.php:So essentially every screenshot first synced after about Jul 21 is empty. Older ones are fine, apart from URLs that only appeared in a later release (my folder_protection has 3/5 working: the two empty ones first shipped in a release on Aug 2).
Likely cause. An empty
200meansrealpath()found the cache file, butfile_get_contents()returnedfalse. Since #20, a successful fetch is written withtempnam()and thenrename()d intocache/.tempnam()creates the file with mode0600, andrename()keeps that mode. Ifsync.phpruns as a different user than the web server, which may have changed with the redeploy for #23/#24 around Jul 21,index.phpcan't read the file and echoes nothing. Warning images are written straight intocache/byimagepng()with the default umask, which is why failures display and successes don't.I can't see the server, so this is inferred. It fits every data point above, and one
ls -lcan confirm it, e.g. for one of mine:I'd expect
-rw-------, owned by the user cron runssync.phpas.Possible fix.
plus a one-off
chmod 644of the files already incache/. Runningsync.phpas the web server user would also work.Examples (sources all return
200 image/png, mirror returns an empty200):Happy to open a PR with the
chmodif that helps.weltmaister commentedon Oct 5, 2026
Same here for
photosphere_tours(release 0.4.11, uploaded 2026-10-02 19:19 UTC, after #31 was merged). All four screenshots return 200 with an empty body from the mirror, while the sources serve fine (e.g.https://raw.githubusercontent.com/weltmaister/photosphere_tours/master/screenshots/viewer.jpg→ 200, image/jpeg, 52 KB). Could you trigger a refetch for this app? Thanks!edward-ly commentedon Oct 5, 2026
Yes, feel free to open a PR if you like! We'll review/merge it as soon as we can. Also, perhaps it would be nice to include in
sync.phpa way to check the permissions for existing cache entries and auto-update them to0644if needed (rather than having to manually update the entries ourselves).Unfortunately, refetches are not possible in general with the way the cache currently works. A workaround would be to also change the URLs for any screenshots you wanna update.
fds2610 commentedon Oct 6, 2026
Another affected app: nc_gantt (Gantt Projects), release 2.3.0, uploaded 2026-09-30 11:31 UTC, i.e. after #31 was merged. This is the same app as in #29, which was closed after #31.
All 10 screenshot URLs in apps.json now answer HTTP 200 with an empty body (application/octet-stream, 0 bytes). Until #31 they answered 404 File not found. The sources are fine (e.g. https://codeberg.org/fds2610/NC-Gantt/raw/tag/v2.3.0-b0879cb/screenshots/appstore/uc01-project-structure.png → 200, image/png, < 300 KB).
This matches the tempnam() + rename() → mode 0600 explanation from the comment above, so #32 should fix it. Re-uploading doesn't help, since the cache is only filled once per URL.
Checked on 2026-10-06 against the current apps.json. Happy to re-test once #32 is deployed.
dillardblom commentedon Oct 7, 2026
One more data point, now that #32 is merged: ng_backup, first release (0.9.1) uploaded 2026-10-07 16:20 UTC.
apps.json)https://raw.githubusercontent.com/dillardblom/ng_backup/main/screenshots/1-backups.pnghttps://raw.githubusercontent.com/dillardblom/ng_backup/main/screenshots/2-restore.pnghttps://raw.githubusercontent.com/dillardblom/ng_backup/main/screenshots/3-settings.pngShortly after the upload the mirror answered
File not found(not synced yet); at 20:58 UTC, 1h20 after #32 was merged, all three answer an empty200. So either #32 isn't deployed yet orsync.phphasn't run since. AsensureReadable()also repairs existing entries, these three should start working on the first sync after deployment without a new upload, which makes them a simple check that the deploy worked. I'll re-test then.Thanks @kreotropic and @edward-ly for #32.
kreotropic commentedon Oct 8, 2026
Hi! Just confirmed that the fix worked! I had to open it in a private window because the cache is forever...
Thanks!