Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807))

### Fixes

- The SDK now logs a specific error message when envelopes are rejected due to size limits (HTTP 413) ([#4863](https://github.com/getsentry/sentry-dotnet/pull/4863))

### Dependencies

- Bump Native SDK from v0.12.2 to v0.12.3 ([#4832](https://github.com/getsentry/sentry-dotnet/pull/4832))
Expand Down
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