🐛 Fixed bookmark card favicons not loading after the first save#28300
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR changes how the oEmbed service generates filenames for stored images. The Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
d911882 to
6e2b01a
Compare
c38a4d2 to
74b025d
Compare
ref https://linear.app/ghost/issue/ONC-1788/bookmark-card-favicon-fetching-broken - content-addressed favicon storage (#27926) made re-saving an already-stored favicon hit the same key, and production object storage rejects in-place overwrites, so the save failed and the card showed the placeholder icon - a random key never collides, so the save always succeeds without needing overwrite permissions or reconstructing the existing file's URL (which we can't reliably derive from config to match what the storage adapter returns) - trade-off: loses within-site dedup; favicons are small, and a random key still avoids the per-write uniqueness walk that #27926 was fixing
74b025d to
d5e832b
Compare
rob-ghost
left a comment
There was a problem hiding this comment.
I believe this solves the issue in a tactical way 👍🏻 I do have some questions / feedback.
For favicons (tiny) this is acceptable
If I'm not mistaken this is both favicons and thumbnails, so the size impact is larger than stated. Might still be fine, but I want us to be clear about the cost of the duplication.
I'm also getting a bit of a 🚩 on the overall design. My thinking:
The fact that we can't overwrite files is a policy decision on our specific adapter. That policy now leaks into the client: because we know we can't overwrite, the OEmbed service has to work around it. That's information leakage through the interface; our internal policy has reached into a storage concern that self-hosters (raw files, or backends that overwrite happily) don't share.
The real fix is to change the adapter so the policy stays behind the port. We can't add a required method without breaking self-hosters, but we can do it non-breakingly: add a method that just takes the buffer and a logical path and returns a URL: cacheImage(buffer, path) with a default implementation that delegates to today's create-and-uniquify behaviour (so every existing adapter is unchanged), overridden in our adapter to hash-and-reuse instead of overwriting.
The key point is that how the bytes are made unique or deduped; content hash, overwrite, random suffix, whatever, is the adapter's business, not the caller's. The SHA is an implementation detail of our adapter's dedupe, so it shouldn't appear at the call site at all. The caller just hands over the bytes and a name; identity, overwrite policy, and URL construction all sit behind the port.
I'd deliberately not fold this into saveRaw itself, that would change behaviour for every saveRaw caller, whereas a new method means the only thing that changes is the favicon/thumbnail path. saveRaw and its other callers stay exactly as they are.
Happy with this change as it is, but I think there's potential for a more principled fix if we have the appetite for it!
Yes you're right, I should have been more clear about that! Though it's worth mentioning that this is how it functioned before the previous "fix", so I feel fine about the change! RE: The policy leaking - yes I do agree. There's also the idea of adding a flag to I'd like to merge this fix as is, to close off the immediate problem - but I'm going to take a note to look at an alternative, and I'll get back to you whether I go through with it or not. |
ref https://linear.app/ghost/issue/ONC-1788/bookmark-card-favicon-fetching-broken
Problem
#27926 switched oEmbed image storage to content-addressed filenames (
name-<sha256>.ext) to kill the per-write uniqueness walk that didn't scale on GCS.Side effect: re-bookmarking a URL whose favicon was already stored hashes to the same path, so
saveRawattempts an in-place overwrite. Production object storage deliberately rejects overwrites (create-only, anti-corruption), so the save throws and the bookmark card silently falls back to the placeholder icon. Invisible in development because local disk overwrites silently.Fix
Name the file with a random suffix instead of a content hash:
Every save lands on a fresh key, so
saveRawalways creates and never overwrites — the no-overwrite restriction is sidestepped entirely. No exists-check, no catch, no URL reconstruction.Why not check-then-reuse the existing file
That was the previous approach in this PR (check
exists, reuse if present). It works for the write, but to return the icon URL for an already-stored file you have to reconstruct it (urlUtils.urlFor) rather than take it fromsaveRaw. We can't guarantee that reconstruction matches what the storage adapter actually returns (it depends onurls:imageconfig reproducing the adapter's CDN + per-tenant key scheme), and the adapter interface is a public extension point so we can't add agetDownloadUrlto fetch it. A wrong reconstructed URL would silently re-break favicons. A unique key avoids the whole question:saveRawalways runs and returns the adapter's own URL.Why a random key is safe re: the original incident
The scaling incident #27926 fixed was the
generateUniquewalk — sequential HEADs probing for a freefavicon-N.png. A random key is unique without probing, so it avoids the walk just as well as a content hash did. No regression there.Trade-off (deliberate)
We lose the within-site dedup the content hash gave us: re-bookmarking the same favicon now stores another copy instead of reusing one. For favicons (tiny) this is acceptable. Note that under create-only these duplicates can't be reclaimed (no delete), so it's a slow, bounded, append-only growth — flagged here so it's a conscious choice, not an accident.