diff --git a/src/StackExchange.Utils.Http/Extensions.Modifier.cs b/src/StackExchange.Utils.Http/Extensions.Modifier.cs
index aa70c4f..2a8119c 100644
--- a/src/StackExchange.Utils.Http/Extensions.Modifier.cs
+++ b/src/StackExchange.Utils.Http/Extensions.Modifier.cs
@@ -91,6 +91,18 @@ public static IRequestBuilder WithoutLogging(this IRequestBuilder builder, HttpS
return builder;
}
+ ///
+ /// Logs error response body as part of the httpClientException data when the response's HTTP status code is any of the .
+ ///
+ /// The builder we're working on.
+ /// HTTP error status codes to log for.
+ /// The request builder for chaining.
+ public static IRequestBuilder WithErrorResponseBodyLogging(this IRequestBuilder builder, params HttpStatusCode[] statusCodes)
+ {
+ builder.LogErrorResponseBodyStatuses = statusCodes;
+ return builder;
+ }
+
///
/// Adds an event handler for this request, for appending additional information to the logged exception for example.
///
diff --git a/src/StackExchange.Utils.Http/Http.cs b/src/StackExchange.Utils.Http/Http.cs
index 0aec09e..37c8974 100644
--- a/src/StackExchange.Utils.Http/Http.cs
+++ b/src/StackExchange.Utils.Http/Http.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics;
+using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
@@ -68,6 +69,14 @@ internal static async Task> SendAsync(IRequestBuilder
if (!response.IsSuccessStatusCode && !builder.Inner.IgnoredResponseStatuses.Contains(response.StatusCode))
{
exception = new HttpClientException($"Response code was {(int)response.StatusCode} ({response.StatusCode}) from {response.RequestMessage.RequestUri}: {response.ReasonPhrase}", response.StatusCode, response.RequestMessage.RequestUri);
+
+ if (builder.Inner.LogErrorResponseBodyStatuses.Contains(response.StatusCode))
+ {
+ using var responseStream = await response.Content.ReadAsStreamAsync();
+ using var streamReader = new StreamReader(responseStream);
+ exception.AddLoggedData("Response.Body", await streamReader.ReadToEndAsync());
+ }
+
stackTraceString.SetValue(exception, new StackTrace(true).ToString());
}
else
diff --git a/src/StackExchange.Utils.Http/HttpBuilder.cs b/src/StackExchange.Utils.Http/HttpBuilder.cs
index b0cccd2..cae0ea1 100644
--- a/src/StackExchange.Utils.Http/HttpBuilder.cs
+++ b/src/StackExchange.Utils.Http/HttpBuilder.cs
@@ -13,6 +13,7 @@ internal class HttpBuilder : IRequestBuilder
public HttpRequestMessage Message { get; }
public bool LogErrors { get; set; } = true;
public IEnumerable IgnoredResponseStatuses { get; set; } = Enumerable.Empty();
+ public IEnumerable LogErrorResponseBodyStatuses { get; set; } = Enumerable.Empty();
public TimeSpan Timeout { get; set; }
public IWebProxy Proxy { get; set; }
public bool BufferResponse { get; set; } = true;
diff --git a/src/StackExchange.Utils.Http/IRequestBuilder.cs b/src/StackExchange.Utils.Http/IRequestBuilder.cs
index cff49f1..d639306 100644
--- a/src/StackExchange.Utils.Http/IRequestBuilder.cs
+++ b/src/StackExchange.Utils.Http/IRequestBuilder.cs
@@ -29,6 +29,12 @@ public interface IRequestBuilder
[EditorBrowsable(EditorBrowsableState.Never)]
bool LogErrors { get; set; }
+ ///
+ /// Whether to log error response body on a given http status code.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ IEnumerable LogErrorResponseBodyStatuses { get; set; }
+
///
/// Which s to ignore as errors on responses.
///
diff --git a/tests/StackExchange.Utils.Tests/HttpLogErrorResponseBodyTest.cs b/tests/StackExchange.Utils.Tests/HttpLogErrorResponseBodyTest.cs
new file mode 100644
index 0000000..de5ede3
--- /dev/null
+++ b/tests/StackExchange.Utils.Tests/HttpLogErrorResponseBodyTest.cs
@@ -0,0 +1,137 @@
+using System.Net;
+using System.Threading.Tasks;
+using HttpMock;
+using Xunit;
+
+namespace StackExchange.Utils.Tests
+{
+ public class HttpLogErrorResponseBodyTest
+ {
+ private readonly IHttpServer _stubHttp = HttpMockRepository.At("http://localhost:9191");
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_IfStatusCodeMatchesTheGivenOne_IncludeResponseBodyInException()
+ {
+ const string errorResponseBody = "{'Foo': 'Bar'}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(errorResponseBody)
+ .WithStatus(HttpStatusCode.UnprocessableEntity);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .WithErrorResponseBodyLogging(HttpStatusCode.UnprocessableEntity)
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.UnprocessableEntity,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Equal(errorResponseBody, httpCallResponses.Error.Data[Http.DefaultSettings.ErrorDataPrefix + "Response.Body"]);
+ }
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_IfStatusCodeMatchesOneOfTheGiven_IncludeResponseBodyInException()
+ {
+ const string errorResponseBody = "{'Foo': 'Bar'}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(errorResponseBody)
+ .WithStatus(HttpStatusCode.UnprocessableEntity);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .WithErrorResponseBodyLogging(HttpStatusCode.NotAcceptable, HttpStatusCode.UnprocessableEntity)
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.UnprocessableEntity,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Equal(errorResponseBody, httpCallResponses.Error.Data[Http.DefaultSettings.ErrorDataPrefix + "Response.Body"]);
+ }
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_IfStatusCodeDoesNotMatchAnyOfTheGiven_DoesNotIncludeResponseBodyInException()
+ {
+ const string errorResponseBody = "{'Foo': 'Bar'}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(errorResponseBody)
+ .WithStatus(HttpStatusCode.UnprocessableEntity);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .WithErrorResponseBodyLogging(HttpStatusCode.NotAcceptable, HttpStatusCode.BadRequest)
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.UnprocessableEntity,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Null(httpCallResponses.Error.Data[Http.DefaultSettings.ErrorDataPrefix + "Response.Body"]);
+ }
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_IfNoStatusCodesGiven_DoesNotIncludeResponseBodyInException()
+ {
+ const string errorResponseBody = "{'Foo': 'Bar'}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(errorResponseBody)
+ .WithStatus(HttpStatusCode.UnprocessableEntity);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .WithErrorResponseBodyLogging()
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.UnprocessableEntity,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Null(httpCallResponses.Error.Data[Http.DefaultSettings.ErrorDataPrefix + "Response.Body"]);
+ }
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_WithoutCallingWithErrorResponseBodyLogging_DoesNotIncludeResponseBodyInException()
+ {
+ const string errorResponseBody = "{'Foo': 'Bar'}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(errorResponseBody)
+ .WithStatus(HttpStatusCode.UnprocessableEntity);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.UnprocessableEntity,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Null(httpCallResponses.Error.Data[Http.DefaultSettings.ErrorDataPrefix + "Response.Body"]);
+ }
+
+ [Fact]
+ public async Task WithErrorResponseBodyLogging_IfResponseSuccess_DoesNotIncludeResponseBodyInExceptionAndDeserializesCorrectly()
+ {
+ const string successResponseBody = @"{""SomeAttribute"": ""some value""}";
+ _stubHttp.Stub(x => x.Get("/some-endpoint"))
+ .Return(successResponseBody)
+ .WithStatus(HttpStatusCode.OK);
+
+ var response = await Http
+ .Request("http://localhost:9191/some-endpoint")
+ .WithErrorResponseBodyLogging(HttpStatusCode.UnprocessableEntity)
+ .ExpectJson()
+ .GetAsync();
+
+ Assert.Equal(HttpStatusCode.OK,response.StatusCode);
+
+ var httpCallResponses = Assert.IsType>(response);
+ Assert.Null(httpCallResponses.Error);
+ Assert.Equal("some value", httpCallResponses.Data.SomeAttribute);
+ }
+ }
+
+ public class SomeResponseObject
+ {
+ public string SomeAttribute { get; set; }
+ }
+}
diff --git a/tests/StackExchange.Utils.Tests/StackExchange.Utils.Tests.csproj b/tests/StackExchange.Utils.Tests/StackExchange.Utils.Tests.csproj
index 90d7c5b..a8dbec3 100644
--- a/tests/StackExchange.Utils.Tests/StackExchange.Utils.Tests.csproj
+++ b/tests/StackExchange.Utils.Tests/StackExchange.Utils.Tests.csproj
@@ -3,6 +3,7 @@
netcoreapp3.1
+