diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 0174dbd32a..edb432dc5b 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -155,8 +155,10 @@ internal async Task SerializeAsync( { try { - await item.SerializeAsync(stream, logger, cancellationToken).ConfigureAwait(false); - await stream.WriteNewlineAsync(cancellationToken).ConfigureAwait(false); + if (await item.SerializeAsync(stream, logger, dropEmptyAttachment: true, cancellationToken).ConfigureAwait(false)) + { + await stream.WriteNewlineAsync(cancellationToken).ConfigureAwait(false); + } } catch (Exception e) { @@ -180,8 +182,10 @@ internal void Serialize(Stream stream, IDiagnosticLogger? logger, ISystemClock c { try { - item.Serialize(stream, logger); - stream.WriteNewline(); + if (item.Serialize(stream, logger, dropEmptyAttachment: true)) + { + stream.WriteNewline(); + } } catch (Exception e) { diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 364d15087e..3b1e84f7a6 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -175,7 +175,11 @@ private static void SerializeHeader( } /// - public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, + public Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, + CancellationToken cancellationToken = default) => + SerializeAsync(stream, logger, dropEmptyAttachment: false, cancellationToken); + + internal async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, bool dropEmptyAttachment, CancellationToken cancellationToken = default) { // Always calculate the length of the payload, as Sentry will reject envelopes that have incorrect lengths @@ -189,6 +193,11 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, await using (payloadBuffer.ConfigureAwait(false)) #endif { + if (dropEmptyAttachment && IsEmptyAttachmentOfUnknownLength(payloadBuffer, logger)) + { + return false; + } + // Write to the outbound stream asynchronously. It's likely either an HttpRequestStream or a FileStream. // Header @@ -199,11 +208,15 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, // Payload await payloadBuffer.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); + return true; } } /// - public void Serialize(Stream stream, IDiagnosticLogger? logger) + public void Serialize(Stream stream, IDiagnosticLogger? logger) => + Serialize(stream, logger, dropEmptyAttachment: false); + + internal bool Serialize(Stream stream, IDiagnosticLogger? logger, bool dropEmptyAttachment) { // Always calculate the length of the payload, as Sentry will reject envelopes that have incorrect lengths // in item headers. Don't trust any previously calculated value to be correct. @@ -211,6 +224,11 @@ public void Serialize(Stream stream, IDiagnosticLogger? logger) using var payloadBuffer = BufferPayload(logger); + if (dropEmptyAttachment && IsEmptyAttachmentOfUnknownLength(payloadBuffer, logger)) + { + return false; + } + // Header var headerWithLength = Header.ToDict(); headerWithLength[LengthKey] = payloadBuffer.Length; @@ -219,6 +237,18 @@ public void Serialize(Stream stream, IDiagnosticLogger? logger) // Payload payloadBuffer.CopyTo(stream); + return true; + } + + private bool IsEmptyAttachmentOfUnknownLength(MemoryStream payloadBuffer, IDiagnosticLogger? logger) + { + if (payloadBuffer.Length != 0 || TryGetLength() is not null || Payload is not BufferedStreamSerializable) + { + return false; + } + + logger?.LogWarning("Did not add '{0}' to envelope because the attachment was empty.", TryGetFileName()); + return true; } /// diff --git a/test/Sentry.Tests/Protocol/Envelopes/AttachmentSerializationTests.cs b/test/Sentry.Tests/Protocol/Envelopes/AttachmentSerializationTests.cs index 061fdaddca..69b2d7229d 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/AttachmentSerializationTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/AttachmentSerializationTests.cs @@ -389,4 +389,70 @@ public void FromEvent_EmptyByteAttachment_DoesNotAddAttachment() .Should() .Be(0); } + + [Fact] + public async Task Serialization_EmptyNonSeekableStreamAttachment_DropsAttachment() + { + // Arrange + using var envelope = CreateEventEnvelopeWithEmptyNonSeekableAttachment(); + + // Act + var serialized = await envelope.SerializeToStringAsync(_testOutputLogger, _fakeClock); + + // Assert + serialized.Should().NotContain("empty.jpg").And.NotContain("\n\n"); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized)); + using var deserialized = await Envelope.DeserializeAsync(stream); + deserialized.Items.Should().ContainSingle() + .Which.TryGetType().Should().Be(EnvelopeItem.TypeValueEvent); + } + + [Fact] + public async Task Serialization_EmptyNonSeekableStreamAttachmentSynchronously_DropsAttachment() + { + // Arrange + using var envelope = CreateEventEnvelopeWithEmptyNonSeekableAttachment(); + + // Act + var serialized = envelope.SerializeToString(_testOutputLogger, _fakeClock); + + // Assert + serialized.Should().NotContain("empty.jpg").And.NotContain("\n\n"); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(serialized)); + using var deserialized = await Envelope.DeserializeAsync(stream); + deserialized.Items.Should().ContainSingle() + .Which.TryGetType().Should().Be(EnvelopeItem.TypeValueEvent); + } + + [Fact] + public async Task Serialization_EmptyAttachmentOfKnownLengthAfterRoundTrip_KeepsAttachment() + { + // Arrange + var attachment = new SentryAttachment( + AttachmentType.Default, + new StreamAttachmentContent(new MemoryStream()), + "empty.log", + "text/plain"); + using var envelope = Envelope.FromAttachment(SentryId.Create(), attachment, _testOutputLogger); + using var cached = new MemoryStream(Encoding.UTF8.GetBytes( + await envelope.SerializeToStringAsync(_testOutputLogger, _fakeClock))); + using var deserialized = await Envelope.DeserializeAsync(cached); + + // Act + var serialized = await deserialized.SerializeToStringAsync(_testOutputLogger, _fakeClock); + + // Assert + serialized.Should().Contain("empty.log"); + } + + private Envelope CreateEventEnvelopeWithEmptyNonSeekableAttachment() + { + var attachment = new SentryAttachment( + AttachmentType.Default, + new StreamAttachmentContent(new NonSeekableReadStream(new MemoryStream())), + "empty.jpg", + "image/jpeg"); + + return Envelope.FromEvent(new SentryEvent(), _testOutputLogger, [attachment]); + } }