Skip to content

Commit 1d5f4c4

Browse files
authored
Fix date signing for s3 path (#296)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches S3 request signing for cloud uploads; wrong encoding would break uploads, but the change is narrow and covered by a focused test. > > **Overview** > Fixes **AWS SigV4** signing for S3 uploads when object keys include hive-style segments such as `date=2026-07-12`. > > `signS3Request` now builds the canonical request URI with a new **`awsCanonicalURI`** helper instead of **`req.URL.EscapedPath()`**, so characters like `=` are percent-encoded the way S3 expects (e.g. `date%3D2026-07-12`). That aligns the signature with the real object path and should stop auth failures on partitioned runtime keys. > > A unit test covers encoding of `=` in those paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit af017da. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
2 parents 608076d + af017da commit 1d5f4c4

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

cli/beacon-hooks/internal/cloudshuttle/shuttle.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ func signS3Request(req *http.Request, cfg Config, payloadHash string) {
503503
signedHeaders := strings.Join(keys, ";")
504504
canonicalRequest := strings.Join([]string{
505505
req.Method,
506-
req.URL.EscapedPath(),
506+
awsCanonicalURI(req.URL.Path),
507507
req.URL.RawQuery,
508508
canonicalHeaders.String(),
509509
signedHeaders,
@@ -680,6 +680,30 @@ func escapePath(value string) string {
680680
return (&url.URL{Path: value}).EscapedPath()
681681
}
682682

683+
func awsCanonicalURI(pathValue string) string {
684+
if pathValue == "" {
685+
return "/"
686+
}
687+
var out strings.Builder
688+
for i := 0; i < len(pathValue); i++ {
689+
b := pathValue[i]
690+
switch {
691+
case b == '/':
692+
out.WriteByte('/')
693+
case (b >= 'A' && b <= 'Z') ||
694+
(b >= 'a' && b <= 'z') ||
695+
(b >= '0' && b <= '9') ||
696+
b == '-' || b == '_' || b == '.' || b == '~':
697+
out.WriteByte(b)
698+
default:
699+
out.WriteByte('%')
700+
out.WriteByte("0123456789ABCDEF"[b>>4])
701+
out.WriteByte("0123456789ABCDEF"[b&0x0f])
702+
}
703+
}
704+
return out.String()
705+
}
706+
683707
func defaultString(value, fallback string) string {
684708
if strings.TrimSpace(value) == "" {
685709
return fallback

cli/beacon-hooks/internal/cloudshuttle/shuttle_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ func TestUploadSendsJSONLToS3(t *testing.T) {
270270
}
271271
}
272272

273+
func TestAWSCanonicalURIEncodesHivePartitionEquals(t *testing.T) {
274+
got := awsCanonicalURI("/bucket/prefix/runtime/date=2026-07-12/1783891419-claude_code_web-cse_123.jsonl.gz")
275+
want := "/bucket/prefix/runtime/date%3D2026-07-12/1783891419-claude_code_web-cse_123.jsonl.gz"
276+
if got != want {
277+
t.Fatalf("awsCanonicalURI = %q, want %q", got, want)
278+
}
279+
}
280+
273281
func TestUploadReusesLastObjectForSameRun(t *testing.T) {
274282
current := time.Date(2026, 7, 12, 20, 17, 3, 0, time.UTC)
275283
restore := stubNowFunc(t, func() time.Time { return current })

0 commit comments

Comments
 (0)