From a72776379c670a132d2d5b5fc2208f34de32a067 Mon Sep 17 00:00:00 2001 From: Bruno Gale Date: Thu, 1 Oct 2026 14:48:36 +0200 Subject: [PATCH] fix(storage): don't send headers from an earlier upload --- .../Files/StorageFileApiContractTests.cs | 20 ++++ packages/Storage/Storage/StorageFileApi.cs | 98 +++++++++++-------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/packages/Storage/Storage.Tests/Files/StorageFileApiContractTests.cs b/packages/Storage/Storage.Tests/Files/StorageFileApiContractTests.cs index 55d7a5b7..2b3e1535 100644 --- a/packages/Storage/Storage.Tests/Files/StorageFileApiContractTests.cs +++ b/packages/Storage/Storage.Tests/Files/StorageFileApiContractTests.cs @@ -264,6 +264,26 @@ public async Task Upload_ShouldComposeStorageHeadersFromFileOptions() } } + [TestMethod] + public async Task Upload_ShouldNotSendHeadersFromAnEarlierUpload() + { + var signedUrl = new UploadSignedUrl( + new Uri($"{this.server.Url}/storage/v1/object/upload/sign/{Bucket}/a.bin?token=abc"), + "abc", + "a.bin"); + this.Respond($"/storage/v1/object/upload/sign/{Bucket}/a.bin", "POST", 200, "{\"Key\":\"x\"}"); + this.Respond($"/storage/v1/object/{Bucket}/b.bin", "POST", 200, "{\"Key\":\"x\"}"); + var bucket = this.client.From(Bucket); + await bucket.UploadToSignedUrl(new byte[] { 0x1 }, signedUrl, new FileOptions { Upsert = true }); + await bucket.Upload(new byte[] { 0x1 }, "b.bin"); + var request = this.server.LogEntries.Last().RequestMessage!; + using (new AssertionScope()) + { + this.HeaderOf(request, "Authorization").Should().Be("Bearer test-key", "a signed upload's token must not replace the client's key on later uploads"); + request.Headers.Should().NotContainKey("x-upsert", "an upsert on one upload must not make later uploads on the same bucket overwrite"); + } + } + [TestMethod] public async Task Upload_ShouldReportProgress_GivenAProgressHandler() { diff --git a/packages/Storage/Storage/StorageFileApi.cs b/packages/Storage/Storage/StorageFileApi.cs index 094b4ee4..aa0b8e53 100644 --- a/packages/Storage/Storage/StorageFileApi.cs +++ b/packages/Storage/Storage/StorageFileApi.cs @@ -392,17 +392,19 @@ public async Task UploadToSignedUrl( if (inferContentType) options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(localFilePath); - this.StorageHeader.Add("Authorization", $"Bearer {signedUrl.Token}"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); - this.StorageHeader.Add("content-type", options.ContentType); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("Authorization", $"Bearer {signedUrl.Token}"); + headers.Add("cache-control", $"max-age={options.CacheControl}"); + headers.Add("content-type", options.ContentType); if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) - this.StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata)); + headers.Add("x-metadata", ParseMetadata(options.Metadata)); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); var progress = new Progress(); @@ -412,9 +414,9 @@ public async Task UploadToSignedUrl( await this.uploadClient.UploadFileAsync( signedUrl.SignedUrl, localFilePath, - this.StorageHeader.Get(), + headers.Get(), progress - ); + ).ConfigureAwait(false); return this.GetFinalPath(signedUrl.Key); } @@ -441,17 +443,19 @@ public async Task UploadToSignedUrl( if (inferContentType) options.ContentType = MimeMapping.MimeUtility.GetMimeMapping(signedUrl.Key); - this.StorageHeader.Add("Authorization", $"Bearer {signedUrl.Token}"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); - this.StorageHeader.Add("content-type", options.ContentType); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("Authorization", $"Bearer {signedUrl.Token}"); + headers.Add("cache-control", $"max-age={options.CacheControl}"); + headers.Add("content-type", options.ContentType); if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) - this.StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata)); + headers.Add("x-metadata", ParseMetadata(options.Metadata)); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); var progress = new Progress(); @@ -461,9 +465,9 @@ public async Task UploadToSignedUrl( await this.uploadClient.UploadBytesAsync( signedUrl.SignedUrl, data, - this.StorageHeader.Get(), + headers.Get(), progress - ); + ).ConfigureAwait(false); return this.GetFinalPath(signedUrl.Key); } @@ -832,26 +836,28 @@ private async Task UploadOrUpdate( { var uri = new Uri($"{this.Url}/object/{this.GetFinalPath(supabasePath)}"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); - this.StorageHeader.Add("content-type", options.ContentType); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("cache-control", $"max-age={options.CacheControl}"); + headers.Add("content-type", options.ContentType); if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) - this.StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata)); + headers.Add("x-metadata", ParseMetadata(options.Metadata)); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); if (options.Duplex != null) - this.StorageHeader.Add("x-duplex", options.Duplex.ToLower()); + headers.Add("x-duplex", options.Duplex.ToLower()); var progress = new Progress(); if (onProgress != null) progress.ProgressChanged += onProgress; - await this.uploadClient.UploadFileAsync(uri, localPath, this.StorageHeader.Get(), progress, cancellationToken); + await this.uploadClient.UploadFileAsync(uri, localPath, headers.Get(), progress, cancellationToken).ConfigureAwait(false); return this.GetFinalPath(supabasePath); } @@ -866,7 +872,9 @@ private async Task UploadOrContinue( { var uri = new Uri($"{this.Url}/upload/resumable"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("cache-control", $"max-age={options.CacheControl}"); var metadata = new MetadataCollection { @@ -876,15 +884,15 @@ private async Task UploadOrContinue( }; if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) - this.StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata)); + headers.Add("x-metadata", ParseMetadata(options.Metadata)); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); if (options.Duplex != null) - this.StorageHeader.Add("x-duplex", options.Duplex.ToLower()); + headers.Add("x-duplex", options.Duplex.ToLower()); var progress = new Progress(); @@ -895,10 +903,10 @@ await this.uploadClient.UploadOrContinueFileAsync( uri, localPath, metadata, - this.StorageHeader.Get(), + headers.Get(), progress, cancellationToken - ); + ).ConfigureAwait(false); } private async Task UploadOrContinue( @@ -911,7 +919,9 @@ private async Task UploadOrContinue( { var uri = new Uri($"{this.Url}/upload/resumable"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("cache-control", $"max-age={options.CacheControl}"); var metadata = new MetadataCollection { @@ -921,15 +931,15 @@ private async Task UploadOrContinue( }; if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) metadata["metadata"] = JsonSerializer.Serialize(options.Metadata, Helpers.SerializerOptions); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); if (options.Duplex != null) - this.StorageHeader.Add("x-duplex", options.Duplex.ToLower()); + headers.Add("x-duplex", options.Duplex.ToLower()); var progress = new Progress(); @@ -940,10 +950,10 @@ await this.uploadClient.UploadOrContinueByteAsync( uri, data, metadata, - this.StorageHeader.Get(), + headers.Get(), progress, cancellationToken - ); + ).ConfigureAwait(false); } private static string ParseMetadata(Dictionary metadata) @@ -964,26 +974,28 @@ private async Task UploadOrUpdate( { var uri = new Uri($"{this.Url}/object/{this.GetFinalPath(supabasePath)}"); - this.StorageHeader.Add("cache-control", $"max-age={options.CacheControl}"); - this.StorageHeader.Add("content-type", options.ContentType); + var headers = new Header(); + headers.Add(this.StorageHeader.Get()); + headers.Add("cache-control", $"max-age={options.CacheControl}"); + headers.Add("content-type", options.ContentType); if (options.Upsert) - this.StorageHeader.Add("x-upsert", options.Upsert.ToString().ToLower()); + headers.Add("x-upsert", options.Upsert.ToString().ToLower()); if (options.Metadata != null) - this.StorageHeader.Add("x-metadata", ParseMetadata(options.Metadata)); + headers.Add("x-metadata", ParseMetadata(options.Metadata)); - options.Headers?.ToList().ForEach(x => this.StorageHeader.Add(x.Key, x.Value)); + options.Headers?.ToList().ForEach(x => headers.Add(x.Key, x.Value)); if (options.Duplex != null) - this.StorageHeader.Add("x-duplex", options.Duplex.ToLower()); + headers.Add("x-duplex", options.Duplex.ToLower()); var progress = new Progress(); if (onProgress != null) progress.ProgressChanged += onProgress; - await this.uploadClient.UploadBytesAsync(uri, data, this.StorageHeader.Get(), progress, cancellationToken); + await this.uploadClient.UploadBytesAsync(uri, data, headers.Get(), progress, cancellationToken).ConfigureAwait(false); return this.GetFinalPath(supabasePath); }