From 252a037e072fe0af3b9fc669253abc00c7689abf Mon Sep 17 00:00:00 2001 From: Bruno Gale Date: Tue, 29 Sep 2026 22:30:45 +0200 Subject: [PATCH 1/2] fix(postgrest): throw a PostgrestException for a non-JSON 2xx body --- .../Requests/NonJsonResponseTests.cs | 66 +++++++++++++++++++ packages/Postgrest/Postgrest/Client.cs | 5 +- packages/Postgrest/Postgrest/Helpers.cs | 20 ++++++ .../Postgrest/Responses/ModeledResponse.cs | 2 +- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 packages/Postgrest/Postgrest.Tests/Requests/NonJsonResponseTests.cs diff --git a/packages/Postgrest/Postgrest.Tests/Requests/NonJsonResponseTests.cs b/packages/Postgrest/Postgrest.Tests/Requests/NonJsonResponseTests.cs new file mode 100644 index 00000000..e860b053 --- /dev/null +++ b/packages/Postgrest/Postgrest.Tests/Requests/NonJsonResponseTests.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Supabase.Postgrest; +using Supabase.Postgrest.Attributes; +using Supabase.Postgrest.Exceptions; +using Supabase.Postgrest.Models; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace Postgrest.Tests.Requests; + +/// +/// A 2xx body that isn't JSON throws a carrying the body and status. +/// +[TestClass] +[TestCategory("Contract")] +public class NonJsonResponseTests +{ + private const string ProxyErrorPage = "502 Bad Gateway"; + + private WireMockServer server = null!; + private Client client = null!; + + [Table("todos")] + private class Todo : BaseModel + { + [PrimaryKey("id")] public int Id { get; set; } + [Column("name")] public string? Name { get; set; } + } + + [TestInitialize] + public void SetUp() + { + this.server = WireMockServer.Start(); + this.client = new Client(this.server.Url!, new ClientOptions()); + } + + [TestCleanup] + public void TearDown() => this.server.Stop(); + + [TestMethod] + public async Task Get_ShouldThrowPostgrestException_GivenNonJsonBody() + { + this.MockProxyErrorPage("/todos"); + var act = () => this.client.Table().Get(); + await act.Should().ThrowAsync() + .Where(exception => exception.Content == ProxyErrorPage && exception.StatusCode == 200, + "postgrest-js returns a non-JSON 2xx body as the error (#260)"); + } + + [TestMethod] + public async Task Rpc_ShouldThrowPostgrestException_GivenNonJsonBody() + { + this.MockProxyErrorPage("/rpc/echo"); + var act = () => this.client.Rpc("echo"); + await act.Should().ThrowAsync() + .Where(exception => exception.Content == ProxyErrorPage && exception.StatusCode == 200, + "postgrest-js handles rpc responses the same way (#260)"); + } + + private void MockProxyErrorPage(string path) => + this.server.Given(Request.Create().WithPath(path)) + .RespondWith(Response.Create().WithStatusCode(200).WithBody(ProxyErrorPage)); +} diff --git a/packages/Postgrest/Postgrest/Client.cs b/packages/Postgrest/Postgrest/Client.cs index d0063531..6755fb4b 100644 --- a/packages/Postgrest/Postgrest/Client.cs +++ b/packages/Postgrest/Postgrest/Client.cs @@ -108,7 +108,10 @@ public T Attach(T model) where T : BaseModel { var response = await this.Rpc(procedureName, parameters); - return string.IsNullOrEmpty(response.Content) ? default : JsonSerializer.Deserialize(response.Content!, SerializerSettings(this.Options)); + if (string.IsNullOrEmpty(response.Content)) return default; + + Helpers.ParseJsonBody(response); + return JsonSerializer.Deserialize(response.Content!, SerializerSettings(this.Options)); } /// diff --git a/packages/Postgrest/Postgrest/Helpers.cs b/packages/Postgrest/Postgrest/Helpers.cs index fecdcfa5..0206709e 100644 --- a/packages/Postgrest/Postgrest/Helpers.cs +++ b/packages/Postgrest/Postgrest/Helpers.cs @@ -175,6 +175,26 @@ HttpRequestMessage CreateRequest() } } + /// + /// Parses a 2xx body and throws a PostgrestException when it isn't JSON, e.g. a proxy's HTML page. + /// + internal static JsonNode? ParseJsonBody(BaseResponse response) + { + try + { + return JsonNode.Parse(response.Content!); + } + catch (JsonException e) + { + throw new PostgrestException(response.Content, e) + { + Content = response.Content, + Response = response.ResponseMessage, + StatusCode = (int) (response.ResponseMessage?.StatusCode ?? 0) + }; + } + } + /// /// Prepares the request with appropriate HTTP headers expected by Postgrest. /// diff --git a/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs b/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs index 9f21726c..d0b04265 100644 --- a/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs +++ b/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs @@ -38,7 +38,7 @@ public ModeledResponse(BaseResponse baseResponse, JsonSerializerOptions serializ if (!shouldParse || string.IsNullOrEmpty(this.Content)) return; - var token = JsonNode.Parse(this.Content!); + var token = Helpers.ParseJsonBody(this); switch (token) { From 67813031c2448ac23935badffeab7ad2fa9e7f53 Mon Sep 17 00:00:00 2001 From: Bruno Gale Date: Wed, 30 Sep 2026 20:04:29 +0200 Subject: [PATCH 2/2] chore(postgrest): deserialize the body inside the try catch --- packages/Postgrest/Postgrest/Client.cs | 5 +---- packages/Postgrest/Postgrest/Helpers.cs | 6 +++--- packages/Postgrest/Postgrest/Responses/ModeledResponse.cs | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/Postgrest/Postgrest/Client.cs b/packages/Postgrest/Postgrest/Client.cs index 6755fb4b..04ce8320 100644 --- a/packages/Postgrest/Postgrest/Client.cs +++ b/packages/Postgrest/Postgrest/Client.cs @@ -108,10 +108,7 @@ public T Attach(T model) where T : BaseModel { var response = await this.Rpc(procedureName, parameters); - if (string.IsNullOrEmpty(response.Content)) return default; - - Helpers.ParseJsonBody(response); - return JsonSerializer.Deserialize(response.Content!, SerializerSettings(this.Options)); + return string.IsNullOrEmpty(response.Content) ? default : Helpers.DeserializeBody(response, SerializerSettings(this.Options)); } /// diff --git a/packages/Postgrest/Postgrest/Helpers.cs b/packages/Postgrest/Postgrest/Helpers.cs index 0206709e..19eb4a7b 100644 --- a/packages/Postgrest/Postgrest/Helpers.cs +++ b/packages/Postgrest/Postgrest/Helpers.cs @@ -176,13 +176,13 @@ HttpRequestMessage CreateRequest() } /// - /// Parses a 2xx body and throws a PostgrestException when it isn't JSON, e.g. a proxy's HTML page. + /// Deserializes a 2xx body and throws a PostgrestException when it can't be read, e.g. a proxy's HTML page. /// - internal static JsonNode? ParseJsonBody(BaseResponse response) + internal static T? DeserializeBody(BaseResponse response, JsonSerializerOptions options) { try { - return JsonNode.Parse(response.Content!); + return JsonSerializer.Deserialize(response.Content!, options); } catch (JsonException e) { diff --git a/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs b/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs index d0b04265..ef71e2f1 100644 --- a/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs +++ b/packages/Postgrest/Postgrest/Responses/ModeledResponse.cs @@ -38,7 +38,7 @@ public ModeledResponse(BaseResponse baseResponse, JsonSerializerOptions serializ if (!shouldParse || string.IsNullOrEmpty(this.Content)) return; - var token = Helpers.ParseJsonBody(this); + var token = Helpers.DeserializeBody(this, serializerSettings); switch (token) {