Integration
sentry-apache-http-client-5
Java Version
17
Other Error Monitoring Solution
No
Other Error Monitoring Solution Name
No response
Version
8.21.1 (also present on main, and in every release since the module was added in #1114)
Steps to Reproduce
ApacheHttpClientTransport.send reads the gzip buffer inside the try-with-resources that owns the GZIPOutputStream, i.e. before close():
https://github.com/getsentry/sentry-java/blob/main/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java
try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
options.getSerializer().serialize(envelopeWithClientReport, gzip);
final SimpleHttpRequest request = SimpleHttpRequests.post(requestDetails.getUrl().toString());
request.setBody(
outputStream.toByteArray(), ContentType.create("application/x-sentry-envelope")); // <-- before close()
request.setHeader("Content-Encoding", "gzip");
Deflater buffers its compressed output and only emits the final deflate block plus the 8-byte gzip trailer (CRC32 + ISIZE) on close()/finish(). JsonSerializer.serialize deliberately does not close the stream it is handed ("we do not want to close these as we would also close the stream that was passed in"), and flush() on a GZIPOutputStream constructed without syncFlush does not flush the deflater either. So at the point toByteArray() is called, the buffer holds only the 10-byte gzip header.
To reproduce end-to-end: configure the SDK with options.setTransportFactory(new ApacheHttpClientTransportFactory()) and capture anything. Every envelope is rejected.
The truncation itself reproduces with no Sentry code at all, mirroring what the serializer does to the stream:
byte[] payload = ("{\"event_id\":\"9ec79c33ec9942ab8353589fcb2e04dc\"}\n"
+ "{\"type\":\"event\",\"length\":42}\n" + "x".repeat(400)).getBytes("UTF-8");
byte[] body;
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out)) {
BufferedOutputStream bos = new BufferedOutputStream(gzip); // as JsonSerializer does
bos.write(payload);
bos.flush();
body = out.toByteArray(); // read before close(), as the transport does
}
System.out.println(payload.length + " bytes in -> " + body.length + " bytes out");
new GZIPInputStream(new ByteArrayInputStream(body)).readAllBytes(); // throws
469 bytes in -> 10 bytes out
Exception in thread "main" java.io.EOFException: Unexpected end of ZLIB input stream
Moving toByteArray() after the try-with-resources block (or calling gzip.finish() before reading) produces a complete, decodable stream.
Expected Result
The POST body is a complete gzip stream containing the serialized envelope, and Sentry accepts it — the same body the default HttpConnection-based transport produces. HttpConnection gets this right incidentally, because it gzips straight into the connection's output stream and so never reads a buffer early.
Actual Result
The body is always the bare 10-byte gzip header: no payload, no trailer, regardless of envelope size. Sentry/Relay rejects 100% of envelopes:
HTTP 400 Failed to buffer the request body: unexpected end of file
Verified against a Relay instance: the same envelope sent uncompressed, or gzipped with the ordering fixed, returns HTTP 200; gzipped the way this transport does it returns the 400 above.
Two things make this hard to notice, which is presumably why the module has shipped this way since #1114:
- The failure is silent. The transport reports it via
options.getLogger(), and DiagnosticLogger.isEnabled returns false unless options.isDebug() is set, so by default every event is dropped with no output at all. We lost five days of production error reporting before noticing, and only found it by reading Relay's own request logs.
Content-Encoding: gzip is set unconditionally, so there is no non-gzip path that would have worked.
Possibly relevant to #4742 ("Verify that the Apache Transport works ... Make a decision on how to move forward with it") — this is the answer to the first bullet: as shipped, it cannot deliver an envelope. Happy to send a PR with the reorder plus a test asserting the request body inflates back to the serialized envelope, if useful.
Integration
sentry-apache-http-client-5
Java Version
17
Other Error Monitoring Solution
No
Other Error Monitoring Solution Name
No response
Version
8.21.1 (also present on
main, and in every release since the module was added in #1114)Steps to Reproduce
ApacheHttpClientTransport.sendreads the gzip buffer inside the try-with-resources that owns theGZIPOutputStream, i.e. beforeclose():https://github.com/getsentry/sentry-java/blob/main/sentry-apache-http-client-5/src/main/java/io/sentry/transport/apache/ApacheHttpClientTransport.java
Deflaterbuffers its compressed output and only emits the final deflate block plus the 8-byte gzip trailer (CRC32 + ISIZE) onclose()/finish().JsonSerializer.serializedeliberately does not close the stream it is handed ("we do not want to close these as we would also close the stream that was passed in"), andflush()on aGZIPOutputStreamconstructed withoutsyncFlushdoes not flush the deflater either. So at the pointtoByteArray()is called, the buffer holds only the 10-byte gzip header.To reproduce end-to-end: configure the SDK with
options.setTransportFactory(new ApacheHttpClientTransportFactory())and capture anything. Every envelope is rejected.The truncation itself reproduces with no Sentry code at all, mirroring what the serializer does to the stream:
Moving
toByteArray()after the try-with-resources block (or callinggzip.finish()before reading) produces a complete, decodable stream.Expected Result
The POST body is a complete gzip stream containing the serialized envelope, and Sentry accepts it — the same body the default
HttpConnection-based transport produces.HttpConnectiongets this right incidentally, because it gzips straight into the connection's output stream and so never reads a buffer early.Actual Result
The body is always the bare 10-byte gzip header: no payload, no trailer, regardless of envelope size. Sentry/Relay rejects 100% of envelopes:
Verified against a Relay instance: the same envelope sent uncompressed, or gzipped with the ordering fixed, returns
HTTP 200; gzipped the way this transport does it returns the400above.Two things make this hard to notice, which is presumably why the module has shipped this way since #1114:
options.getLogger(), andDiagnosticLogger.isEnabledreturns false unlessoptions.isDebug()is set, so by default every event is dropped with no output at all. We lost five days of production error reporting before noticing, and only found it by reading Relay's own request logs.Content-Encoding: gzipis set unconditionally, so there is no non-gzip path that would have worked.Possibly relevant to #4742 ("Verify that the Apache Transport works ... Make a decision on how to move forward with it") — this is the answer to the first bullet: as shipped, it cannot deliver an envelope. Happy to send a PR with the reorder plus a test asserting the request body inflates back to the serialized envelope, if useful.