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..04ce8320 100644 --- a/packages/Postgrest/Postgrest/Client.cs +++ b/packages/Postgrest/Postgrest/Client.cs @@ -108,7 +108,7 @@ 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)); + 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 fecdcfa5..19eb4a7b 100644 --- a/packages/Postgrest/Postgrest/Helpers.cs +++ b/packages/Postgrest/Postgrest/Helpers.cs @@ -175,6 +175,26 @@ HttpRequestMessage CreateRequest() } } + /// + /// Deserializes a 2xx body and throws a PostgrestException when it can't be read, e.g. a proxy's HTML page. + /// + internal static T? DeserializeBody(BaseResponse response, JsonSerializerOptions options) + { + try + { + return JsonSerializer.Deserialize(response.Content!, options); + } + 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..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 = JsonNode.Parse(this.Content!); + var token = Helpers.DeserializeBody(this, serializerSettings); switch (token) {