Skip to content

Commit

Permalink
custom exception factory for deserialization exceptions (#1862)
Browse files Browse the repository at this point in the history
Co-authored-by: Teddy Assefa <[email protected]>
  • Loading branch information
ted-ccm and Teddy Assefa authored Oct 6, 2024
1 parent faa1f68 commit 5331e71
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 17 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,20 @@ var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
});
```

Note that exceptions raised when attempting to deserialize the response are not affected by this.
For exceptions raised when attempting to deserialize the response use DeserializationExceptionFactory described bellow.

#### Providing a custom `DeserializationExceptionFactory`

You can override default deserialization exceptions behavior that are raised by the `DeserializationExceptionFactory` when processing the result by providing a custom exception factory in `RefitSettings`. For example, you can suppress all deserialization exceptions with the following:

```csharp
var nullTask = Task.FromResult<Exception>(null);

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
DeserializationExceptionFactory = (httpResponse, exception) => nullTask;
});
```

#### `ApiException` deconstruction with Serilog

Expand Down
154 changes: 154 additions & 0 deletions Refit.Tests/DeserializationExceptionFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using System.Net;
using System.Net.Http;
using RichardSzalay.MockHttp;
using Xunit;

namespace Refit.Tests;

public class DeserializationExceptionFactoryTests
{
public interface IMyService
{
[Get("/get-with-result")]
Task<int> GetWithResult();
}

[Fact]
public async Task NoDeserializationExceptionFactory_WithSuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
};

var intContent = 123;
handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent($"{intContent}"));

var fixture = RestService.For<IMyService>("http://api", settings);

var result = await fixture.GetWithResult();

handler.VerifyNoOutstandingExpectation();

Assert.Equal(intContent, result);
}

[Fact]
public async Task NoDeserializationExceptionFactory_WithUnsuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
};

handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent("non-int-result"));

var fixture = RestService.For<IMyService>("http://api", settings);

var thrownException = await Assert.ThrowsAsync<ApiException>(() => fixture.GetWithResult());
Assert.Equal("An error occured deserializing the response.", thrownException.Message);

handler.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task ProvideFactoryWhichReturnsNull_WithSuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
DeserializationExceptionFactory = (_, _) => Task.FromResult<Exception>(null)
};

var intContent = 123;
handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent($"{intContent}"));

var fixture = RestService.For<IMyService>("http://api", settings);

var result = await fixture.GetWithResult();

handler.VerifyNoOutstandingExpectation();

Assert.Equal(intContent, result);
}

[Fact]
public async Task ProvideFactoryWhichReturnsNull_WithUnsuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
DeserializationExceptionFactory = (_, _) => Task.FromResult<Exception>(null)
};

handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent("non-int-result"));

var fixture = RestService.For<IMyService>("http://api", settings);

var result = await fixture.GetWithResult();

handler.VerifyNoOutstandingExpectation();

Assert.Equal(default, result);
}

[Fact]
public async Task ProvideFactoryWhichReturnsException_WithUnsuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var exception = new Exception("Unsuccessful Deserialization Exception");
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
DeserializationExceptionFactory = (_, _) => Task.FromResult<Exception>(exception)
};

handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent("non-int-result"));

var fixture = RestService.For<IMyService>("http://api", settings);

var thrownException = await Assert.ThrowsAsync<Exception>(() => fixture.GetWithResult());
Assert.Equal(exception, thrownException);

handler.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task ProvideFactoryWhichReturnsException_WithSuccessfulDeserialization()
{
var handler = new MockHttpMessageHandler();
var exception = new Exception("Unsuccessful Deserialization Exception");
var settings = new RefitSettings()
{
HttpMessageHandlerFactory = () => handler,
DeserializationExceptionFactory = (_, _) => Task.FromResult<Exception>(exception)
};

var intContent = 123;
handler
.Expect(HttpMethod.Get, "http://api/get-with-result")
.Respond(HttpStatusCode.OK, new StringContent($"{intContent}"));

var fixture = RestService.For<IMyService>("http://api", settings);

var result = await fixture.GetWithResult();

handler.VerifyNoOutstandingExpectation();

Assert.Equal(intContent, result);
}
}
6 changes: 6 additions & 0 deletions Refit/RefitSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ public Func<
/// </summary>
public Func<HttpResponseMessage, Task<Exception?>> ExceptionFactory { get; set; }

/// <summary>
/// Supply a function to provide <see cref="Exception"/> when deserialization exception is encountered.
/// If function returns null - no exception is thrown.
/// </summary>
public Func<HttpResponseMessage, Exception, Task<Exception?>>? DeserializationExceptionFactory { get; set; }

/// <summary>
/// Defines how requests' content should be serialized. (defaults to <see cref="SystemTextJsonContentSerializer"/>)
/// </summary>
Expand Down
47 changes: 31 additions & 16 deletions Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,19 @@ object itemValue
catch (Exception ex)
{
//if an error occured while attempting to deserialize return the wrapped ApiException
e = await ApiException.Create(
"An error occured deserializing the response.",
resp.RequestMessage!,
resp.RequestMessage!.Method,
resp,
settings,
ex
);
if (settings.DeserializationExceptionFactory != null)
e = await settings.DeserializationExceptionFactory(resp, ex).ConfigureAwait(false);
else
{
e = await ApiException.Create(
"An error occured deserializing the response.",
resp.RequestMessage!,
resp.RequestMessage!.Method,
resp,
settings,
ex
);
}
}

return ApiResponse.Create<T, TBody>(
Expand All @@ -387,14 +392,24 @@ e as ApiException
}
catch (Exception ex)
{
throw await ApiException.Create(
"An error occured deserializing the response.",
resp.RequestMessage!,
resp.RequestMessage!.Method,
resp,
settings,
ex
);
if (settings.DeserializationExceptionFactory != null)
{
var customEx = await settings.DeserializationExceptionFactory(resp, ex).ConfigureAwait(false);
if (customEx != null)
throw customEx;
return default;
}
else
{
throw await ApiException.Create(
"An error occured deserializing the response.",
resp.RequestMessage!,
resp.RequestMessage!.Method,
resp,
settings,
ex
);
}
}
}
}
Expand Down

0 comments on commit 5331e71

Please sign in to comment.