fix(storage): normalize slashes in storage basePath - #1249
Merged
Conversation
`createStorageKeyBuilder` joined `basePath` with the rest of the key as given, so a `basePath` of `/releases/` produced `/releases//bundles/<id>/bundle.zip` instead of `releases/bundles/<id>/bundle.zip`. The empty segment is not cosmetic. `createStorageRootUriWithPath` drops empty path segments when it derives `assetBaseStorageUri` from the bundle `storageUri`, so deploy records shared asset URIs under `releases/assets/...` while the same asset is uploaded to `/releases//assets/...`. The `exists` check never matches, every asset is re-uploaded on each deploy, and the manifest points at keys that were never written. `s3Storage` already strips the surrounding slashes itself before building its keys, which is why AWS is unaffected. Doing it in the shared helper fixes the r2, supabase and firebase plugins, plus any custom plugin built on `createStorageKeyBuilder` as the storage plugin guide recommends.
🦋 Changeset detectedLatest commit: 52cd8c4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 25 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
|
@giaBaoJS is attempting to deploy a commit to the hot-updater Team on Vercel. A member of the Team first needs to authorize it. |
@hot-updater/android-helper
@hot-updater/apple-helper
@hot-updater/bsdiff
@hot-updater/cli-tools
@hot-updater/console
@hot-updater/core
hot-updater
@hot-updater/react-native
@hot-updater/server
@hot-updater/aws
@hot-updater/bare
@hot-updater/bugsnag-plugin
@hot-updater/cloudflare
@hot-updater/datadog-plugin
@hot-updater/expo
@hot-updater/firebase
@hot-updater/js
@hot-updater/plugin-core
@hot-updater/postgres
@hot-updater/rock
@hot-updater/sentry-plugin
@hot-updater/standalone
@hot-updater/supabase
commit: |
gronxb
approved these changes
Sep 6, 2026
gronxb
left a comment
Owner
There was a problem hiding this comment.
Confirmed that slash-wrapped basePath values produce mismatched asset keys, and this fix restores consistent upload and manifest paths. Thanks for the fix and regression coverage!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
createStorageKeyBuilderjoins the configured storagebasePathwith the rest of the key exactly as given:A
basePathof/releases/therefore yields/releases//bundles/<id>/bundle.zipinstead ofreleases/bundles/<id>/bundle.zip.s3Storageis unaffected because it strips the surrounding slashes itself before building its keys:r2Storage(both the S3 and Wrangler profiles),supabaseStorageandfirebaseStorageall passconfig.basePathstraight through, so only those three carry the empty segment.s3Storage.spec.tsalready usesbasePath: "/releases/"as a normal input, so this shape is expected to work.Reproduce
Configure any of r2 / supabase / firebase storage with
basePath: "/releases/"and deploy a bundle with shared assets.The empty segment is not cosmetic, because the bundle
storageUriis parsed back into a key elsewhere:uploadwrites the bundle to/releases//bundles/<id>/bundle.zipand returnsr2://bucket//releases//bundles/<id>/bundle.zip.deployderives the shared asset root withcreateStorageRootUriWithPath, which splits the pathname and calls.filter(Boolean). That drops the empty segment, givingr2://bucket/releases/assets.r2://bucket/releases/assets/sha256/<aa>/<hash>.png, whose key parses asreleases/assets/....getStorageKeyagain, landing at/releases//assets/sha256/<aa>/<hash>.png.The two keys never agree.
existsmisses on every asset, so deploy re-uploads all of them on every run, and the manifest points at keys that were never written.Root cause
The shared key builder does not normalize
basePath, so a leading or trailing slash becomes an empty path segment that the asset URI resolver later discards.The fix
Strip the surrounding slashes in
createStorageKeyBuilder, matching whats3Storagealready does.s3Storagekeeps its own normalization and is unchanged, since the operation is idempotent. Fixing the shared helper also covers custom plugins, whichdocs/content/docs/storage-plugins/custom-storage.mdxtells authors to build oncreateStorageKeyBuilder.What the tests assert
plugins/plugin-core/src/createStorageKeyBuilder.spec.ts(new) assertscreateStorageKeyBuilder("/releases/")("bundles/id", "bundle.zip")returnsreleases/bundles/id/bundle.zip, and that abasePathof""or"/"contributes no segment.plugins/supabase/src/supabaseStorage.spec.tsadds an end-to-end case asserting that withbasePath: "/releases/"the plugin callsbucket.uploadwithreleases/bundles/bundle.zip. Without the fix it is called with/releases//bundles/bundle.zip.pnpm -w lintis clean and the existing storage suites (s3, r2, firebase, supabase, bundle/asset storage layout,storage prune) still pass.