diff --git a/CHANGELOG.md b/CHANGELOG.md index 6afa5fc97a..462a62b445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 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)) - Fixed thread-safety issue on Android when multiple events are captured concurrently ([#4814](https://github.com/getsentry/sentry-dotnet/pull/4814)) ### Dependencies diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs index 681818e824..d19bae1961 100644 --- a/src/Sentry/Http/HttpTransportBase.cs +++ b/src/Sentry/Http/HttpTransportBase.cs @@ -506,7 +506,7 @@ private void IncrementDiscardsForHttpFailure(HttpStatusCode responseStatusCode, return; } - _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.NetworkError, envelope); + _options.ClientReportRecorder.RecordDiscardedEvents(DiscardReason.SendError, envelope); // 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"); @@ -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, @@ -536,6 +542,15 @@ private void LogFailure(JsonElement responseJson, HttpStatusCode responseStatusC responseJson.GetPropertyOrNull("causes")?.EnumerateArray().Select(j => j.GetString()).ToArray() ?? Array.Empty(); + 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, @@ -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); + } + private static bool HasJsonContent(HttpContent content) => string.Equals(content.Headers.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase); diff --git a/src/Sentry/Internal/DiscardReason.cs b/src/Sentry/Internal/DiscardReason.cs index afc71bd3e2..a75a24566f 100644 --- a/src/Sentry/Internal/DiscardReason.cs +++ b/src/Sentry/Internal/DiscardReason.cs @@ -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"); diff --git a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs index 2b4ec36cc8..aa828064c9 100644 --- a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs @@ -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(); @@ -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 }); } @@ -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(); + + httpHandler.VerifiableSendAsync(Arg.Any(), Arg.Any()) + .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(); + + httpHandler.VerifiableSendAsync(Arg.Any(), Arg.Any()) + .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)); + } }