fix(storage): encode keys per-segment to avoid SigV4 path double-escape#101
Conversation
`copy.ts`, `move.ts` (via `copyOrMove`), and `updateObject` built request paths and `X-Amz-Copy-Source` headers with plain `encodeURIComponent(key)`. When the custom HTTP client signs with SigV4 (access-key auth), the signer URI-escapes the path again during canonical-request construction. A `/` in the key became `%2F` and then `%252F`. The gateway decodes `%2F` back to `/` before computing its own canonical path, so the two canonical paths diverge and the server returns `403 SignatureDoesNotMatch`. OAuth/session-token callers were unaffected — the custom HTTP client skips SigV4 in that branch — which is why the bug only surfaced when the CLI integration suite started exercising `copy`/`move` with access keys. Adds `encodeObjectKey` in `@shared/utils` that splits on `/`, per-segment encodes, and rejoins. All four call sites in `copy.ts` and `update.ts` switch to it. Adds five unit tests covering flat keys, nested keys (regression-tagged), special chars within a segment, trailing slashes (folder markers), and empty input. Documents the SigV4 encoding rule and scope (path required; header recommended; query strings and AWS-SDK paths unaffected) in AGENTS.md so the gotcha is discoverable next time it almost gets re-introduced. Assisted-by: Claude Opus 4.7 via Claude Code
Greptile SummaryFixes
Confidence Score: 3/5The The core
Important Files Changed
Reviews (1): Last reviewed commit: "fix(storage): encode keys per-segment to..." | Re-trigger Greptile |
| access-key auth branch), `SignatureV4.sign()` URI-escapes the request | ||
| path *again* during canonical-request construction. If the key was | ||
| already escaped with plain `encodeURIComponent`, every `/` becomes | ||
| `%2F` and then `%252F`. The Tigris gateway decodes `%2F` back to `/` |
There was a problem hiding this comment.
Per the repository's naming convention, "the" should not appear as a definite article directly before "Tigris". The same phrase also appears in
.changeset/sigv4-path-encoding.md.
| `%2F` and then `%252F`. The Tigris gateway decodes `%2F` back to `/` | |
| `%2F` and then `%252F`. Tigris gateway decodes `%2F` back to `/` |
Rule Used: Don't use the definite article 'the' before 'Tigri... (source)
Learned From
tigrisdata/tigris-os-docs#262
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 397fcbe. Configure here.
`@smithy/signature-v4`'s `SignatureV4` defaults `uriEscapePath: true`, which is the AWS-standard double-encoding scheme. S3 uses single-encoding, and Tigris gateway follows S3 semantics. With the default, the signer re-percent-encoded any path sequence during canonicalization (e.g. `%20` → `%2520`) while the gateway treated the wire path as single-encoded — producing `SignatureDoesNotMatch` for every key with a character that requires percent-encoding (space, `?`, `=`, `&`, etc.). The per-segment `encodeObjectKey` fix from the previous commit was necessary but not sufficient: it kept `/` intact for valid URLs but left every other special char exposed to the signer's re-canonicalization pass. Adds `uriEscapePath: false` to the `SignatureV4` constructor in the custom HTTP client. Verified end-to-end against access-key auth: `cp src.txt 'folder/my file.txt'` now succeeds (was 403 Forbidden SignatureDoesNotMatch). AGENTS.md updated to describe both encoding traps (the signer setting and per-segment pre-encoding) and to require integration tests with both a nested key and a special-char key. Changeset updated to reflect both fixes. Addresses greptile review on #101. Assisted-by: Claude Opus 4.7 via Claude Code

Summary
copy,move, andupdateObjectreturned403 SignatureDoesNotMatchwhen the object key contained/and the caller used access-key auth. This PR fixes the underlying encoding mistake and documents the gotcha for next time.Root cause
The custom HTTP client (
shared/http-client.ts) signs requests with SigV4 when access keys are present.SignatureV4.sign()URI-escapes the request path again during canonical-request construction. The affected functions were building paths and theX-Amz-Copy-Sourceheader with plainencodeURIComponent(key), so every/became%2F, then%252Fafter the signer's pass. The Tigris gateway decodes%2Fback to/before computing its own canonical path, so the two canonical paths diverged and the signature didn't match.OAuth/session-token callers were unaffected — the custom client takes a different branch that skips SigV4 entirely — which is why this only surfaced once the CLI integration suite started exercising
copy/movewith access keys.Changes
shared/utils.ts— newencodeObjectKey(key)helper: splits on/, encodes each segment withencodeURIComponent, rejoins. JSDoc explains the SigV4 reason.packages/storage/src/lib/object/copy.ts— both call sites (request path andX-Amz-Copy-Sourceheader) switched toencodeObjectKey. Fixesmovetransitively (usescopyOrMove).packages/storage/src/lib/object/update.ts— same two-call-site fix in the deprecatedupdateObjectso existing callers don't trip on it either.shared/utils.test.ts— five new tests for the helper: flat key, nested key (regression), special chars within a segment, trailing slash (folder markers), empty input.AGENTS.md— new "Known pitfalls" section with the SigV4 rule, scope (path required; header recommended; query strings and AWS-SDK paths unaffected), symptom checklist, and code snippet for new contributors..changeset/sigv4-path-encoding.md— patch bump for@tigrisdata/storage.Verification
pnpm test: 145 storage tests pass (5 new), all other packages green.pnpm check(biome): clean.pnpm build: clean.cp t3://bucket/flat.txt t3://bucket/nested/file.txt: was403 SignatureDoesNotMatch, now200.mv t3://bucket/nested/file.txt t3://bucket/nested/renamed.txt -f: same path viacopyOrMove, now works.Test plan for reviewer
shared/utils.test.tsexercise the intended cases.copy/moveagainst a personal bucket with access keys and a key likefolder/file.txt.Assisted-by: Claude Opus 4.7 via Claude Code
Note
Medium Risk
Touches request-path and copy-source encoding for SigV4-signed custom HTTP client calls; mistakes here can break copy/move/rename behavior for keys with special characters, but scope is limited and covered by new unit tests.
Overview
Fixes
copy/moveand deprecatedupdateObjectfailing with403 SignatureDoesNotMatchwhen object keys contain/under access-key (SigV4) auth by switching fromencodeURIComponent(key)to a new per-segmentencodeObjectKeyhelper.Adds
encodeObjectKey(with tests) and documents the SigV4 path-encoding pitfall inAGENTS.md; includes a changeset to publish a patch for@tigrisdata/storage.Reviewed by Cursor Bugbot for commit 397fcbe. Bugbot is set up for automated code reviews on this repo. Configure here.