Skip to content

🐛 Fixed bookmark favicon storage on object-storage adapters#27670

Closed
rob-ghost wants to merge 2 commits intomainfrom
fix/onc-1673-bookmark-favicons
Closed

🐛 Fixed bookmark favicon storage on object-storage adapters#27670
rob-ghost wants to merge 2 commits intomainfrom
fix/onc-1673-bookmark-favicons

Conversation

@rob-ghost
Copy link
Copy Markdown
Contributor

@rob-ghost rob-ghost commented May 5, 2026

Problem

Bookmark cards stopped rendering favicons on Pro because the bookmark fetcher passed an absolute filesystem path to the storage adapter. Investigation revealed the bug was the user-visible symptom of a wider design issue: the storage adapter contract was permissive — accepting absolute paths, paths already prefixed with the storage root, decorative leading slashes, etc. — and its consumers (the bookmark fetcher, the importer image handler, the external-media-inliner, and the image-sizes middleware) each violated it in different ways. The two adapters (LocalStorageBase and S3Storage) tolerated the violations differently, so callers got away with it on local dev while producing malformed bucket keys on object-storage backends.

Solution

Tighten the contract: both adapters now require canonical relative paths via a shared assertCanonicalRelativePath validator. Migrate all four callers to pass the canonical shape. Drops the silent tolerance helpers from LocalStorageBase. Mutating methods (save / saveRaw / delete / read) throw on bad input; exists() is lenient and returns false since it's a query, not a mutation.

The empty string is the only "non-relative-looking" input the validator accepts, and only for targetDir arguments — it represents the storage root itself, which the importer needs for files at the top of an import zip.

Migration

Forward-only. No existing storage objects move or rename; URLs already stored in posts continue to resolve to whatever object they referenced before. New writes from previously-broken callers (importer + media-inliner) now land at the keys their referencing URLs expect.

Comparison with #27671

This is the strict half of a strict-vs-permissive design debate. PR #27671 is the permissive alternative — same user-visible fix, but the adapter absorbs the multi-shape inputs instead of forcing each caller to be correct. Both PRs are open so the contract direction can be picked deliberately.

ref https://linear.app/ghost/issue/ONC-1673

ref https://linear.app/ghost/issue/ONC-1673

processImageFromUrl was joining the absolute getContentPath('images') with
the imageType and passing the result to store.generateUnique. Object-storage
adapters (e.g. S3) build keys via path.posix.join, which preserves an
absolute second argument and produces a malformed bucket key embedding the
local filesystem prefix. The exists() probe then targeted a different key
than the eventual saveRaw, defeating uniqueness checks and -- depending on
bucket policy -- throwing on the HEAD request, which surfaced as the
bookmark favicon fallback users were reporting. Pass the relative imageType
directly so probe and write target the same key. Also converted the six
oembed logging.error sites to structured logging with event names and
bookmark/icon/thumbnail URL context so failures can be attributed to
specific embeds in Elasticsearch.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aeeb90e8-82ad-4623-b5bd-f18bd4c260d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/onc-1673-bookmark-favicons

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

ref https://linear.app/tryghost/issue/ONC-1673

The previous fix patched the bookmark favicon caller, but the underlying
storage adapters silently tolerated absolute, prefixed, and traversing
paths — masking similar bugs elsewhere. Introduced a shared
assertCanonicalRelativePath validator used by LocalStorageBase and
S3Storage at all mutating entry points, removed the tolerance helpers
that papered over bad inputs, and migrated the remaining non-canonical
callers (importer image handler, external media inliner,
handle-image-sizes middleware, getLocalImagesStoragePath) so the
contract holds end-to-end. exists() stays lenient (returns false on
malformed input) since it has a natural negative answer.
@rob-ghost rob-ghost force-pushed the fix/onc-1673-bookmark-favicons branch from b88ef5f to 5768626 Compare May 6, 2026 09:46
@rob-ghost
Copy link
Copy Markdown
Contributor Author

Closing in favour of #27671, which lands the same user-visible fix (favicons restored) with a much smaller diff by absorbing the multi-shape inputs at the adapter layer rather than tightening the contract and migrating callers.

The strict approach in this PR is still defensible — it makes the storage adapter contract explicit and would prevent future callers from reintroducing similar bugs — but the cost (validator module, file-vs-directory distinction, three caller migrations, ~25 test fixtures touched) outweighs the benefit at this point. Re-opening this if we decide later that the permissive contract is causing problems.

@rob-ghost rob-ghost closed this May 6, 2026
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.

1 participant