Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- The `Serilog` integration captures _Structured Logs_ (when enabled) independently of captured Events and added Breadcrumbs ([#4691](https://github.com/getsentry/sentry-dotnet/pull/4691))
- The SDK avoids redundant scope sync after transaction finish ([#4623](https://github.com/getsentry/sentry-dotnet/pull/4623))
- sentry-native is now automatically disabled for WASM applications ([#4631](https://github.com/getsentry/sentry-dotnet/pull/4631))
- Captured [Http Client Errors](https://docs.sentry.io/platforms/dotnet/guides/aspnet/configuration/http-client-errors/) on .NET 6+ now include a full stack trace in order to improve Issue grouping ([#4724](https://github.com/getsentry/sentry-dotnet/pull/4724))

### Dependencies

Expand Down
25 changes: 25 additions & 0 deletions src/Sentry/HttpStatusCodeRangeExtensions.cs
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hesitated about whether or not we need the new extension method (which is pretty trivial and only used in one place in the code base)... it does make the calling code slightly easier to read though so OK to leave this in here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah for sure--the main reason I actually ended up adding it was that it allowed the tests to be better separated. Otherwise there were a bunch of tests of the SentryHttpFailedRequestHandler that were really just testing the FailedRequestStatusCodes handling in all variety of scenarios and became more verbose due to having to setup/run/capture hub events.

With a dedicated extension methods and corresponding tests, the ContainsStatusCode tests are very simple and comprehensive, and the SentryHttpFailedRequestHandler don't have to repeat them.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Sentry;

/// <summary>
/// Extension methods for collections of <see cref="HttpStatusCodeRange"/>.
/// </summary>
internal static class HttpStatusCodeRangeExtensions
{
/// <summary>
/// Checks if any range in the collection contains the given status code.
/// </summary>
/// <param name="ranges">Collection of ranges to check.</param>
/// <param name="statusCode">Status code to check.</param>
/// <returns>True if any range contains the given status code.</returns>
internal static bool ContainsStatusCode(this IEnumerable<HttpStatusCodeRange> ranges, int statusCode)
=> ranges.Any(range => range.Contains(statusCode));

/// <summary>
/// Checks if any range in the collection contains the given status code.
/// </summary>
/// <param name="ranges">Collection of ranges to check.</param>
/// <param name="statusCode">Status code to check.</param>
/// <returns>True if any range contains the given status code.</returns>
internal static bool ContainsStatusCode(this IEnumerable<HttpStatusCodeRange> ranges, HttpStatusCode statusCode)
=> ranges.ContainsStatusCode((int)statusCode);
}
28 changes: 0 additions & 28 deletions src/Sentry/Internal/Extensions/HttpStatusExtensions.cs

This file was deleted.

106 changes: 62 additions & 44 deletions src/Sentry/SentryHttpFailedRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,67 +15,85 @@ internal SentryHttpFailedRequestHandler(IHub hub, SentryOptions options)
protected internal override void DoEnsureSuccessfulResponse([NotNull] HttpRequestMessage request, [NotNull] HttpResponseMessage response)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: update from main, as we've merged the big version6 branch adding support for .NET 10 in the meantime

Copy link
Member

@Flash0ver Flash0ver Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO @Flash0ver approve workflows after update
(currently the build fails ... see test/Sentry.Tests/Internals/Extensions/HttpRequestExceptionMessageTests.cs) and #4724 (comment))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated from main

{
// Don't capture events for successful requests
if (!Options.FailedRequestStatusCodes.Any(range => range.Contains(response.StatusCode)))
if (!Options.FailedRequestStatusCodes.ContainsStatusCode(response.StatusCode))
{
return;
}

// Capture the event
try

var statusCode = (int)response.StatusCode;
// Match behavior of HttpResponseMessage.EnsureSuccessStatusCode
// See https://github.com/getsentry/sentry-dotnet/issues/2684
if (statusCode >= 200 && statusCode <= 299)
{
#if NET5_0_OR_GREATER
response.EnsureSuccessStatusCode();
return;
}

var exception = new HttpRequestException(
string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"Response status code does not indicate success: {0}",
statusCode)
);

#if NET6_0_OR_GREATER
// Add a full stack trace into the exception to improve Issue grouping,
// see https://github.com/getsentry/sentry-dotnet/issues/3582
ExceptionDispatchInfo.SetCurrentStackTrace(exception);
#else
// Use our own implementation of EnsureSuccessStatusCode because older implementations of
// EnsureSuccessStatusCode disposes the content.
// See https://github.com/dotnet/runtime/issues/24845
response.StatusCode.EnsureSuccessStatusCode();
#endif
// Where SetRemoteStackTrace is not available, throw and catch to get a basic stack trace
try
{
throw exception;
}
catch (HttpRequestException exception)
catch (HttpRequestException ex)
{
exception.SetSentryMechanism(MechanismType);
exception = ex;
}
#endif

var @event = new SentryEvent(exception);
var hint = new SentryHint(HintTypes.HttpResponseMessage, response);
exception.SetSentryMechanism(MechanismType);

var uri = response.RequestMessage?.RequestUri;
var sentryRequest = new SentryRequest
{
QueryString = uri?.Query,
Method = response.RequestMessage?.Method.Method.ToUpperInvariant()
};
var @event = new SentryEvent(exception);
var hint = new SentryHint(HintTypes.HttpResponseMessage, response);

var responseContext = new Response
{
StatusCode = (short)response.StatusCode,
var uri = response.RequestMessage?.RequestUri;
var sentryRequest = new SentryRequest
{
QueryString = uri?.Query,
Method = response.RequestMessage?.Method.Method.ToUpperInvariant()
};

var responseContext = new Response
{
StatusCode = (short)response.StatusCode,
#if NET5_0_OR_GREATER
// Starting with .NET 5, the content and headers are guaranteed to not be null.
BodySize = response.Content.Headers.ContentLength
// Starting with .NET 5, the content and headers are guaranteed to not be null.
BodySize = response.Content.Headers.ContentLength
#else
// The ContentLength might be null (but that's ok).
// See https://github.com/dotnet/runtime/issues/16162
BodySize = response.Content?.Headers?.ContentLength
// The ContentLength might be null (but that's ok).
// See https://github.com/dotnet/runtime/issues/16162
BodySize = response.Content?.Headers?.ContentLength
#endif
};
};

if (!Options.SendDefaultPii)
{
sentryRequest.Url = uri?.HttpRequestUrl();
}
else
{
sentryRequest.Url = uri?.AbsoluteUri;
sentryRequest.Cookies = request.Headers.GetCookies();
sentryRequest.AddHeaders(request.Headers);
responseContext.Cookies = response.Headers.GetCookies();
responseContext.AddHeaders(response.Headers);
}
if (!Options.SendDefaultPii)
{
sentryRequest.Url = uri?.HttpRequestUrl();
}
else
{
sentryRequest.Url = uri?.AbsoluteUri;
sentryRequest.Cookies = request.Headers.GetCookies();
sentryRequest.AddHeaders(request.Headers);
responseContext.Cookies = response.Headers.GetCookies();
responseContext.AddHeaders(response.Headers);
}

@event.Request = sentryRequest;
@event.Contexts[Response.Type] = responseContext;
@event.Request = sentryRequest;
@event.Contexts[Response.Type] = responseContext;

Hub.CaptureEvent(@event, hint: hint);
}
Hub.CaptureEvent(@event, hint: hint);
}
}
171 changes: 171 additions & 0 deletions test/Sentry.Tests/HttpStatusCodeRangeExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
namespace Sentry.Tests;

public class HttpStatusCodeRangeExtensionsTests
{
[Fact]
public void ContainsStatusCode_EmptyList_ReturnsFalse()
{
// Arrange
var ranges = new List<HttpStatusCodeRange>();

// Act
var result = ranges.ContainsStatusCode(404);

// Assert
result.Should().BeFalse();
}

[Theory]
[InlineData(400)]
[InlineData(450)]
[InlineData(499)]
public void ContainsStatusCode_SingleRangeInRange_ReturnsTrue(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 499) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeTrue();
}

[Theory]
[InlineData(200)]
[InlineData(399)]
[InlineData(500)]
public void ContainsStatusCode_SingleRangeOutOfRange_ReturnsFalse(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 499) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeFalse();
}

[Theory]
[InlineData(400)] // In first range
[InlineData(404)] // In first range
[InlineData(500)] // In second range
[InlineData(503)] // In second range
public void ContainsStatusCode_MultipleRangesInAnyRange_ReturnsTrue(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 404), (500, 503) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeTrue();
}

[Theory]
[InlineData(200)] // Below ranges
[InlineData(405)] // Between ranges
[InlineData(499)] // Between ranges
[InlineData(504)] // Above ranges
public void ContainsStatusCode_MultipleRangesNotInAnyRange_ReturnsFalse(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 404), (500, 503) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeFalse();
}

[Theory]
[InlineData(400)] // In first range only
[InlineData(425)] // In overlap
[InlineData(450)] // In overlap
[InlineData(475)] // In second range only
public void ContainsStatusCode_OverlappingRangesInUnion_ReturnsTrue(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 450), (425, 475) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeTrue();
}

[Theory]
[InlineData(200)] // Below first range
[InlineData(399)] // Below first range
[InlineData(476)] // Above second range
[InlineData(500)] // Above second range
public void ContainsStatusCode_OverlappingRangesOutsideUnion_ReturnsFalse(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 450), (425, 475) };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeFalse();
}

[Fact]
public void ContainsStatusCode_SingleValueRangeExactMatch_ReturnsTrue()
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { 404 };

// Act
var result = ranges.ContainsStatusCode(404);

// Assert
result.Should().BeTrue();
}

[Theory]
[InlineData(403)]
[InlineData(405)]
public void ContainsStatusCode_SingleValueRangeNoMatch_ReturnsFalse(int statusCode)
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { 404 };

// Act
var result = ranges.ContainsStatusCode(statusCode);

// Assert
result.Should().BeFalse();
}

[Fact]
public void ContainsStatusCode_HttpStatusCodeEnumInRange_ReturnsTrue()
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 499) };

// Act
var result = ranges.ContainsStatusCode(HttpStatusCode.NotFound); // 404

// Assert
result.Should().BeTrue();
}

[Fact]
public void ContainsStatusCode_HttpStatusCodeEnumOutOfRange_ReturnsFalse()
{
// Arrange
var ranges = new List<HttpStatusCodeRange> { (400, 499) };

// Act
var result = ranges.ContainsStatusCode(HttpStatusCode.OK); // 200

// Assert
result.Should().BeFalse();
}
}
Loading
Loading