Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion Storage/Exceptions/FailureHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,60 @@

namespace Supabase.Storage.Exceptions
{
/// <summary>
/// Maps a failed storage response onto a coarse <see cref="Reason"/> a caller can branch on.
/// </summary>
public static class FailureHint
{
/// <summary>
/// A coarse classification of why a storage request failed, derived from its status code and body.
/// </summary>
public enum Reason
{
/// <summary>
/// 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.
/// </summary>
Unknown,

/// <summary>
/// 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).
/// </summary>
NotAuthorized,

/// <summary>
/// The storage service failed internally (HTTP 500).
/// </summary>
Internal,

/// <summary>
/// The requested object or bucket does not exist (HTTP 404).
/// </summary>
NotFound,

/// <summary>
/// The resource being created already exists (HTTP 409).
/// </summary>
AlreadyExists,
InvalidInput

/// <summary>
/// The request was rejected as invalid (an HTTP 400 whose body indicates invalid input).
/// </summary>
InvalidInput,

/// <summary>
/// The upload exceeded the gateway's request-size limit (HTTP 413). Retry it through a
/// resumable upload (<c>UploadOrResume</c>) rather than a single request.
/// </summary>
EntityTooLarge
}

/// <summary>
/// Classifies a failed storage request from its status code and response body.
/// </summary>
/// <param name="storageException">The failure to classify.</param>
/// <returns>The matching <see cref="Reason"/>, or <see cref="Reason.Unknown"/> when nothing matches.</returns>
public static Reason DetectReason(SupabaseStorageException storageException)
{
if (storageException.Content == null)
Expand All @@ -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
};
Expand Down
12 changes: 12 additions & 0 deletions Storage/StorageFileApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ public async Task<string> CreateSignedUrl(
/// <summary>
/// Uploads a file to an existing bucket.
/// </summary>
/// <remarks>
/// This is a single-request upload and is subject to the gateway's request-size limit; a file
/// past that limit fails with a <see cref="SupabaseStorageException"/> of
/// <see cref="FailureHint.Reason.EntityTooLarge"/>. For large files use the resumable
/// <see cref="UploadOrResume(string, string, FileOptions?, EventHandler{float}?, CancellationToken)"/>.
/// </remarks>
/// <param name="localFilePath">File Source Path</param>
/// <param name="supabasePath">The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.</param>
/// <param name="options"></param>
Expand Down Expand Up @@ -274,6 +280,12 @@ public async Task<string> Upload(
/// <summary>
/// Uploads a byte array to an existing bucket.
/// </summary>
/// <remarks>
/// This is a single-request upload and is subject to the gateway's request-size limit; data
/// past that limit fails with a <see cref="SupabaseStorageException"/> of
/// <see cref="FailureHint.Reason.EntityTooLarge"/>. For large payloads use the resumable
/// <see cref="UploadOrResume(byte[], string, FileOptions?, EventHandler{float}?, CancellationToken)"/>.
/// </remarks>
/// <param name="data"></param>
/// <param name="supabasePath">The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.</param>
/// <param name="options"></param>
Expand Down
6 changes: 6 additions & 0 deletions StorageTests/Errors/FailureHintTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<html>413 Request Entity Too Large</html>")).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);
Expand Down
17 changes: 17 additions & 0 deletions StorageTests/Files/StorageFileApiContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,23 @@ public async Task PurgeCache_ShouldDeleteTheCdnObjectPathWithNoBodyAndReturnTheM
}
}

[TestMethod]
public async Task Upload_ShouldSurfaceStorageException_GivenNonJsonError()
{
const string body = "<html><head><title>413 Request Entity Too Large</title></head></html>";
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<SupabaseStorageException>(
"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()
{
Expand Down
Loading