Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 27 additions & 1 deletion src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ private void IncrementDiscardsForHttpFailure(HttpStatusCode responseStatusCode,
return;
}

_options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope);
_options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.SendError, envelope);
Copy link
Contributor Author

@bitsandfoxes bitsandfoxes Jan 22, 2026

Choose a reason for hiding this comment

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

If I read the docs correctly it is expected to now effectively change all NetworkError to SendError, see https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures

If Sentry returns an HTTP 4xx or HTTP 5xx status code, SDKs:

MUST discard the envelope
MUST record a [client report](https://develop.sentry.dev/sdk/telemetry/client-reports/) with the discard reason send_error, except for an HTTP 429 response, see below.

Copy link
Member

Choose a reason for hiding this comment

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

I believe you're right.
Disambiguating the dev-docs via getsentry/sentry-docs#16152.


// Also restore any counts that were trying to be sent, so they are not lost.
var clientReportItems = envelope.Items.Where(x => x.TryGetType() == "client_report");
Expand All @@ -519,6 +519,12 @@ private void IncrementDiscardsForHttpFailure(HttpStatusCode responseStatusCode,

private void LogFailure(string responseString, HttpStatusCode responseStatusCode, SentryId? eventId)
{
if (responseStatusCode == HttpStatusCode.RequestEntityTooLarge)
{
LogRequestTooLarge(eventId, responseString);
return;
}

_options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}.",
_typeName,
eventId,
Expand All @@ -536,6 +542,15 @@ private void LogFailure(JsonElement responseJson, HttpStatusCode responseStatusC
responseJson.GetPropertyOrNull("causes")?.EnumerateArray().Select(j => j.GetString()).ToArray()
?? Array.Empty<string>();

if (responseStatusCode == HttpStatusCode.RequestEntityTooLarge)
{
var responseDetail = errorCauses.Length > 0
? $"{errorMessage} ({string.Join(", ", errorCauses)})"
: errorMessage;
LogRequestTooLarge(eventId, responseDetail);
return;
}

_options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}. Error causes: {4}.",
_typeName,
eventId,
Expand All @@ -544,6 +559,17 @@ private void LogFailure(JsonElement responseJson, HttpStatusCode responseStatusC
string.Join(", ", errorCauses));
}

private void LogRequestTooLarge(SentryId? eventId, string? responseDetail)
{
_options.LogError(
"{0}: Sentry rejected the envelope '{1}' because it exceeded the maximum allowed size. " +
"Consider reducing attachment sizes, removing unnecessary data, or splitting large payloads into smaller requests. " +
"Server response: {2}",
_typeName,
eventId,
responseDetail ?? "No additional details provided");
}

private static bool HasJsonContent(HttpContent content) =>
string.Equals(content.Headers.ContentType?.MediaType, "application/json",
StringComparison.OrdinalIgnoreCase);
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Internal/DiscardReason.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry.Internal;
public static DiscardReason EventProcessor = new("event_processor");
public static DiscardReason NetworkError = new("network_error");
public static DiscardReason QueueOverflow = new("queue_overflow");
public static DiscardReason SendError = new("send_error");
public static DiscardReason RateLimitBackoff = new("ratelimit_backoff");
public static DiscardReason SampleRate = new("sample_rate");
public static DiscardReason Backpressure = new("backpressure");
Expand Down
114 changes: 109 additions & 5 deletions test/Sentry.Tests/Internals/Http/HttpTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,8 @@ public async Task SendEnvelopeAsync_ResponseRequestEntityTooLargeWithoutPathDefi
public async Task SendEnvelopeAsync_ResponseNotOkWithStringMessage_LogsError()
{
// Arrange
const HttpStatusCode expectedCode = HttpStatusCode.RequestEntityTooLarge;
const string expectedMessage = "413 Request Entity Too Large";
const HttpStatusCode expectedCode = HttpStatusCode.InternalServerError;
const string expectedMessage = "500 Internal Server Error";

var httpHandler = Substitute.For<MockableHttpMessageHandler>();

Expand Down Expand Up @@ -483,9 +483,9 @@ public async Task SendEnvelopeAsync_Fails_RestoresDiscardedEventCounts()
{DiscardReason.EventProcessor.WithCategory(DataCategory.Error), 2},
{DiscardReason.QueueOverflow.WithCategory(DataCategory.Security), 3},

// We also expect two new items recorded, due to the forced network failure.
{DiscardReason.NetworkError.WithCategory(DataCategory.Error), 1}, // from the event
{DiscardReason.NetworkError.WithCategory(DataCategory.Default), 1} // from the client report
// We also expect two new items recorded, due to the forced HTTP failure.
{DiscardReason.SendError.WithCategory(DataCategory.Error), 1}, // from the event
{DiscardReason.SendError.WithCategory(DataCategory.Default), 1} // from the client report
});
}

Expand Down Expand Up @@ -885,4 +885,108 @@ public async Task SendEnvelopeAsync_RateLimited_CallsBackpressureMonitor()
backpressureMonitor.LastRateLimitEventTicks.Should().Be(_fakeClock.GetUtcNow().Ticks);
backpressureMonitor.IsHealthy.Should().BeFalse();
}

[Fact]
public async Task SendEnvelopeAsync_Response413WithJsonMessage_LogsSizeLimitError()
{
// Arrange
const string expectedDetail = "Envelope too large";
var expectedCauses = new[] { "max size exceeded" };

var httpHandler = Substitute.For<MockableHttpMessageHandler>();

httpHandler.VerifiableSendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(_ => SentryResponses.GetJsonErrorResponse(HttpStatusCode.RequestEntityTooLarge, expectedDetail, expectedCauses));

var logger = new InMemoryDiagnosticLogger();

var httpTransport = new HttpTransport(
new SentryOptions
{
Dsn = ValidDsn,
Debug = true,
DiagnosticLogger = logger
},
new HttpClient(httpHandler));

var envelope = Envelope.FromEvent(new SentryEvent());

// Act
await httpTransport.SendEnvelopeAsync(envelope);

// Assert
var errorEntry = logger.Entries.FirstOrDefault(e =>
e.Level == SentryLevel.Error &&
e.Message.Contains("exceeded the maximum allowed size"));

errorEntry.Should().NotBeNull();
errorEntry!.Message.Should().Contain("Consider reducing attachment sizes");
errorEntry.Args[2].ToString().Should().Contain(expectedDetail);
errorEntry.Args[2].ToString().Should().Contain(expectedCauses[0]);
}

[Fact]
public async Task SendEnvelopeAsync_Response413WithTextMessage_LogsSizeLimitError()
{
// Arrange
const string expectedMessage = "413 Request Entity Too Large";

var httpHandler = Substitute.For<MockableHttpMessageHandler>();

httpHandler.VerifiableSendAsync(Arg.Any<HttpRequestMessage>(), Arg.Any<CancellationToken>())
.Returns(_ => SentryResponses.GetTextErrorResponse(HttpStatusCode.RequestEntityTooLarge, expectedMessage));

var logger = new InMemoryDiagnosticLogger();

var httpTransport = new HttpTransport(
new SentryOptions
{
Dsn = ValidDsn,
Debug = true,
DiagnosticLogger = logger
},
new HttpClient(httpHandler));

var envelope = Envelope.FromEvent(new SentryEvent());

// Act
await httpTransport.SendEnvelopeAsync(envelope);

// Assert
var errorEntry = logger.Entries.FirstOrDefault(e =>
e.Level == SentryLevel.Error &&
e.Message.Contains("exceeded the maximum allowed size"));

errorEntry.Should().NotBeNull();
errorEntry!.Message.Should().Contain("Consider reducing attachment sizes");
errorEntry.Args[2].ToString().Should().Contain(expectedMessage);
}

[Fact]
public async Task SendEnvelopeAsync_Response413_RecordsSendErrorDiscard()
{
// Arrange
using var httpHandler = new RecordingHttpMessageHandler(
new FakeHttpMessageHandler(
() => SentryResponses.GetJsonErrorResponse(HttpStatusCode.RequestEntityTooLarge, "Too large")));

var options = new SentryOptions
{
Dsn = ValidDsn,
DiagnosticLogger = _testOutputLogger,
SendClientReports = true,
Debug = true
};

var httpTransport = new HttpTransport(options, new HttpClient(httpHandler));

var recorder = (ClientReportRecorder)options.ClientReportRecorder;

// Act
await httpTransport.SendEnvelopeAsync(Envelope.FromEvent(new SentryEvent()));

// Assert - should use SendError, not NetworkError
recorder.DiscardedEvents.Should().ContainKey(DiscardReason.SendError.WithCategory(DataCategory.Error));
recorder.DiscardedEvents.Should().NotContainKey(DiscardReason.NetworkError.WithCategory(DataCategory.Error));
}
}
Loading