From ad5b81c32511731d34d58ccfb7820de3d5e7c4b8 Mon Sep 17 00:00:00 2001 From: tr00d Date: Tue, 4 Aug 2026 10:46:10 +0200 Subject: [PATCH] feat: classify oversized-upload 413s A storage request rejected for exceeding the gateway's request-size limit surfaced correctly as a SupabaseStorageException but was left Reason.Unknown, with no hint at a way forward. Add FailureHint.Reason.EntityTooLarge and map HTTP 413 to it, with the enum value's doc and on both Upload overloads pointing callers at the resumable UploadOrResume. Cover it with a unit test (413 => EntityTooLarge) and a contract test asserting an oversized upload against a 413 text/html body surfaces StatusCode 413 / EntityTooLarge rather than crashing JSON parsing. Also completes the XML documentation for the FailureHint type, clearing its CS1591 debt. Relates to supabase-community/storage-csharp#14 --- Storage/Exceptions/FailureHint.cs | 45 ++++++++++++++++++- Storage/StorageFileApi.cs | 12 +++++ StorageTests/Errors/FailureHintTests.cs | 6 +++ .../Files/StorageFileApiContractTests.cs | 17 +++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/Storage/Exceptions/FailureHint.cs b/Storage/Exceptions/FailureHint.cs index 4bfd3a8..9f98533 100644 --- a/Storage/Exceptions/FailureHint.cs +++ b/Storage/Exceptions/FailureHint.cs @@ -3,18 +3,60 @@ namespace Supabase.Storage.Exceptions { + /// + /// Maps a failed storage response onto a coarse a caller can branch on. + /// public static class FailureHint { + /// + /// A coarse classification of why a storage request failed, derived from its status code and body. + /// public enum Reason { + /// + /// The failure could not be attributed to a known cause — the status and body matched no + /// recognised pattern, or the response carried no body to inspect. + /// Unknown, + + /// + /// The request was rejected for authentication or authorization reasons (HTTP 401, or a + /// 400/403 whose body names an auth cause such as a missing or malformed token). + /// NotAuthorized, + + /// + /// The storage service failed internally (HTTP 500). + /// Internal, + + /// + /// The requested object or bucket does not exist (HTTP 404). + /// NotFound, + + /// + /// The resource being created already exists (HTTP 409). + /// AlreadyExists, - InvalidInput + + /// + /// The request was rejected as invalid (an HTTP 400 whose body indicates invalid input). + /// + InvalidInput, + + /// + /// The upload exceeded the gateway's request-size limit (HTTP 413). Retry it through a + /// resumable upload (UploadOrResume) rather than a single request. + /// + EntityTooLarge } + /// + /// Classifies a failed storage request from its status code and response body. + /// + /// The failure to classify. + /// The matching , or when nothing matches. public static Reason DetectReason(SupabaseStorageException storageException) { if (storageException.Content == null) @@ -31,6 +73,7 @@ 403 when storageException.Content.ToLower().Contains("invalid compact jws") => N 403 when storageException.Content.ToLower().Contains("signature verification failed") => NotAuthorized, 404 when storageException.Content.ToLower().Contains("not found") => NotFound, 409 when storageException.Content.ToLower().Contains("exists") => AlreadyExists, + 413 => EntityTooLarge, 500 => Internal, _ => Unknown }; diff --git a/Storage/StorageFileApi.cs b/Storage/StorageFileApi.cs index 31d8fe3..77282fa 100644 --- a/Storage/StorageFileApi.cs +++ b/Storage/StorageFileApi.cs @@ -246,6 +246,12 @@ public async Task CreateSignedUrl( /// /// Uploads a file to an existing bucket. /// + /// + /// This is a single-request upload and is subject to the gateway's request-size limit; a file + /// past that limit fails with a of + /// . For large files use the resumable + /// . + /// /// File Source Path /// The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. /// @@ -274,6 +280,12 @@ public async Task Upload( /// /// Uploads a byte array to an existing bucket. /// + /// + /// This is a single-request upload and is subject to the gateway's request-size limit; data + /// past that limit fails with a of + /// . For large payloads use the resumable + /// . + /// /// /// The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload. /// diff --git a/StorageTests/Errors/FailureHintTests.cs b/StorageTests/Errors/FailureHintTests.cs index 1749574..064a11b 100644 --- a/StorageTests/Errors/FailureHintTests.cs +++ b/StorageTests/Errors/FailureHintTests.cs @@ -63,6 +63,12 @@ public void DetectReason_ShouldReturnNotFound_Given404NotFound() => public void DetectReason_ShouldReturnAlreadyExists_Given409Exists() => FailureHint.DetectReason(Failure(409, "The resource already exists")).Should().Be(Reason.AlreadyExists); + [TestMethod] + public void DetectReason_ShouldReturnEntityTooLarge_Given413() => + FailureHint.DetectReason(Failure(413, "413 Request Entity Too Large")).Should() + .Be(Reason.EntityTooLarge, + "a 413 is the oversized-upload signal that should steer callers to a resumable upload (issue #14)"); + [TestMethod] public void DetectReason_ShouldReturnInternal_Given500() => FailureHint.DetectReason(Failure(500, "boom")).Should().Be(Reason.Internal); diff --git a/StorageTests/Files/StorageFileApiContractTests.cs b/StorageTests/Files/StorageFileApiContractTests.cs index 7d7f9cb..2f39332 100644 --- a/StorageTests/Files/StorageFileApiContractTests.cs +++ b/StorageTests/Files/StorageFileApiContractTests.cs @@ -336,6 +336,23 @@ public async Task PurgeCache_ShouldDeleteTheCdnObjectPathWithNoBodyAndReturnTheM } } + [TestMethod] + public async Task Upload_ShouldSurfaceStorageException_GivenNonJsonError() + { + const string body = "413 Request Entity Too Large"; + this.server.Given(Request.Create().WithPath($"/storage/v1/object/{Bucket}/big.bin").UsingPost()) + .RespondWith(Response.Create().WithStatusCode(413).WithHeader("Content-Type", "text/html").WithBody(body)); + var act = () => this.client.From(Bucket).Upload(new byte[] { 0x1 }, "big.bin"); + var exception = (await act.Should().ThrowAsync( + "an oversized upload returns a non-JSON gateway error that must not crash JSON parsing (issue #14)")).Which; + using (new AssertionScope()) + { + exception.StatusCode.Should().Be(413); + exception.Content.Should().Be(body); + exception.Reason.Should().Be(FailureHint.Reason.EntityTooLarge); + } + } + [TestMethod] public async Task List_ShouldSurfaceStorageException_GivenNonJsonError() {