Skip to content

fix: drop attachments whose stream turns out empty when the envelope is sent - #5787

Merged
bitsandfoxes merged 4 commits into
mainfrom
fix/drop-empty-attachments
Oct 28, 2026
Merged

bitsandfoxes merged 4 commits into
mainfrom
fix/drop-empty-attachments

Conversation

@bitsandfoxes

@bitsandfoxes bitsandfoxes commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Empty attachments are supposed to be skipped, but an attachment backed by a non-seekable stream that turns out empty gets sent with length: 0, and Relay stores an empty file on the event.

  • Envelope.AddEnvelopeItemFromAttachment only skips an attachment when stream.TryGetLength() returns 0.
  • A non-seekable stream reports null (unknown length), so the item is kept.
  • Its payload is only buffered when the envelope gets serialized, and the header ends up with length: 0.

sentry-unity hits this regularly: its screenshot attachment returns a non-seekable stream so that Envelope.FromEvent doesn't block waiting for the end-of-frame capture. When the screenshot is discarded (SetBeforeSendScreenshot returns null), times out or fails, the event shows an empty screenshot.jpg. See getsentry/sentry-unity#2835.

Fix

Serialization is the first point where the payload size is known, so the check happens there:

  • Envelope.Serialize / SerializeAsync skip an attachment item when it came from an attachment stream of unknown length and its buffered payload is empty. The item header and its trailing newline are both left out.
  • A warning gets logged, worded like the existing checks at envelope creation: Did not add '<file>' to envelope because the attachment was empty.

What stays the same

  • Explicitly empty attachments: items with a known length: 0 are kept. The envelope spec's "Envelope with 2 empty attachments" example, which EnvelopeTests mirrors, serializes as before.
  • Public API: EnvelopeItem.Serialize / SerializeAsync keep their signatures and always write. They delegate to new internal overloads that return whether the item was written, so Envelope can skip the trailing newline as well.
  • Offline caching: the empty item is dropped before the envelope is written to disk. Items read back from the cache are never dropped. Deserialization strips length, so the check keys on the payload type (BufferedStreamSerializable) rather than the header, and a deliberately empty attachment survives the round trip, as it does on main.
  • Client reports: none are recorded for the drop, matching the existing drops at envelope creation.

Edge case

An envelope whose only item gets dropped, e.g. an empty non-seekable attachment sent via Hub.CaptureAttachment, goes out as a header-only envelope. Relay accepts it and stores nothing: handle_managed_envelope returns early when envelope.is_empty(). The only cost is the request.

Tests

AttachmentSerializationTests:

  • Serialization_EmptyNonSeekableStreamAttachment_DropsAttachment and Serialization_EmptyNonSeekableStreamAttachmentSynchronously_DropsAttachment: attach an empty non-seekable stream through Envelope.FromEvent, serialize async and sync, then deserialize again. Both failed before this change.
  • Serialization_EmptyAttachmentOfKnownLengthAfterRoundTrip_KeepsAttachment: round-trips a known-length empty attachment through serialize → deserialize → serialize and checks it's kept.

🤖 Generated with Claude Code

bitsandfoxes and others added 3 commits October 23, 2026 22:09
Non-seekable attachment streams have no known length when the envelope
is created, so the existing zero-length check can't catch them. Skip the
item at serialization time, once its payload is buffered and before its
header is written.

Co-Authored-By: Claude <noreply@anthropic.com>
Keeps explicit length-0 items, as in the envelope spec's framing examples,
serializing as before.

Co-Authored-By: Claude <noreply@anthropic.com>
Deserialization strips the length header, so keying the drop on an unknown
length also dropped deliberately empty attachments after a cache round trip.
Key it on the payload built from a stream of unknown length instead.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 76.15%. Comparing base (ff0355e) to head (dac0cdd).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
src/Sentry/Protocol/Envelopes/EnvelopeItem.cs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5787      +/-   ##
==========================================
+ Coverage   76.13%   76.15%   +0.02%     
==========================================
  Files         528      528              
  Lines       19579    19590      +11     
  Branches     3829     3835       +6     
==========================================
+ Hits        14906    14919      +13     
+ Misses       3791     3790       -1     
+ Partials      882      881       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ric-oliv ric-oliv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One comment, but it looks good to me!

Relay's item parser rejects a trailing lone newline with UnexpectedEof,
dropping the whole envelope, but Envelope.DeserializeAsync skips it, so
no test caught Envelope writing the newline for a dropped item.

Co-Authored-By: Claude <noreply@anthropic.com>
@bitsandfoxes
bitsandfoxes merged commit 3bba861 into main Oct 28, 2026
59 checks passed
@bitsandfoxes
bitsandfoxes deleted the fix/drop-empty-attachments branch October 28, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants