Skip to content

Response body bounds#8224

Open
jack-berg wants to merge 9 commits intoopen-telemetry:mainfrom
jack-berg:response-body-bounds
Open

Response body bounds#8224
jack-berg wants to merge 9 commits intoopen-telemetry:mainfrom
jack-berg:response-body-bounds

Conversation

@jack-berg
Copy link
Copy Markdown
Member

@jack-berg jack-berg commented Mar 30, 2026

Add new max response body size parameter to sender config, defaulting to 4mb.

Per the spec, if a response exceeding the limit is received:

  • grpc: fail with RESOURCE_EXHAUSTED code, do not retry
  • http: fail with exception, do not retry

For OTLP exporters, the body size is not configurable, since we don't do anything with the response today. If adding this 4mb limit causes new exceptions for users, we can add a config option to allow it to be increased.

For JaegerRemoteSampler, a new programmatic config option is added because the responses are critical to the function of the sampler.

cc @brunobat you'll want to update the vertex sender to incorporate this new option.

@jack-berg jack-berg requested a review from a team as a code owner March 30, 2026 14:01
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 82.48175% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.29%. Comparing base (47c970d) to head (95ba71f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...orter/sender/okhttp/internal/OkHttpGrpcSender.java 81.63% 6 Missing and 3 partials ⚠️
...ry/exporter/sender/jdk/internal/JdkHttpSender.java 84.61% 5 Missing and 1 partial ⚠️
...orter/sender/okhttp/internal/OkHttpHttpSender.java 86.20% 2 Missing and 2 partials ⚠️
...ace/jaeger/sampler/JaegerRemoteSamplerBuilder.java 25.00% 3 Missing ⚠️
...ntelemetry/sdk/common/export/GrpcSenderConfig.java 0.00% 1 Missing ⚠️
...ntelemetry/sdk/common/export/HttpSenderConfig.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8224      +/-   ##
============================================
- Coverage     90.31%   90.29%   -0.02%     
- Complexity     7652     7682      +30     
============================================
  Files           843      848       +5     
  Lines         23087    23162      +75     
  Branches       2312     2334      +22     
============================================
+ Hits          20851    20915      +64     
- Misses         1517     1524       +7     
- Partials        719      723       +4     

☔ View full report in Codecov by Sentry.
📢 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.

@Nullable RetryPolicy retryPolicy,
@Nullable ExecutorService executorService) {
@Nullable ExecutorService executorService,
long maxResponseBodySize) {
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.

JaegerRemoteSamplerBuilder.setMaxSamplingStrategyResponseBodySize validates bytes > 0. But the sender constructors (JdkHttpSender, OkHttpHttpSender, OkHttpGrpcSender) accept any long without
validation. A caller can bypass the guard by constructing a sender directly with 0 or -1. Consider adding the validation at the sender level too, or document the expected invariant.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

All the sender constructors are internal, and have a bunch of other unvalidated parameters which could be equally corrupted if a user goes around the guards. There's a conversation here discussing where to add additional null checks beyond the guarantees from nullaway. I think we should adopt a policy of adding additional null checks at the API boundaries, but trusting nullaway once we're within the walled garden of internal code. The same would apply to parameter validation, I think.

* Sets the maximum number of bytes to read from a sampling strategy response body. If unset,
* defaults to 4 MiB. Must be positive.
*/
public JaegerRemoteSamplerBuilder setMaxSamplingStrategyResponseBodySize(long bytes) {
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.

GrpcExporterBuilder and HttpExporterBuilder hardcode 4MB with no setter. Only JaegerRemoteSamplerBuilder exposes a public setter. If a user wants to tighten the OTLP limit (say, to 64KB since responses are tiny), they can't. Worth a deliberate decision: intentionally omit setters for OTLP (since responses are well-known small), or add them for consistency?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I omitted them in OTLP for now because we don't do anything with them and a 4mb limit is an improvement on the current no limit.

In contrast, for JaegerRemoteSampler, the default body size reduction is a change in behavior which could be meaningful so adding the setter seems essential.

@jack-berg
Copy link
Copy Markdown
Member Author

I had an oversight where I was enforcing the size limit against the response pre-decompression.

This means that a response payload at the limit of 4mb, could decompress to a much larger size and have a big impact on memory.

Fixing this means more complexity, including new enforcement that we only support gzip compressed resposnes. This issue has turned in quite the rabbit hole.

On the bright side, there's pretty good abstract tests in AbstractHttpTelemetryExporterTest / AbstractGrpcTelemetryExporterTest that verify that all the sender implementations are doing the right thing.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds response-body size bounding (default 4 MiB) to HTTP/gRPC sender configurations and updates sender implementations/tests to enforce the limit and handle gzip-encoded responses, aligning behavior with the spec guidance (HTTP: fail w/ exception; gRPC: fail w/ RESOURCE_EXHAUSTED).

Changes:

  • Introduce getMaxResponseBodySize() on HttpSenderConfig / GrpcSenderConfig (default 4 MiB) and document sender expectations.
  • Enforce bounded response reads (and gzip handling) in OkHttp and JDK senders; add Jaeger Remote Sampler builder option for sampling strategy response size.
  • Extend OTLP exporter test infrastructure to generate oversized responses and validate failure behavior (including gzip cases).

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java Adds configurable max response body size (default 4 MiB).
sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSender.java Documents max-sized response body expectation.
sdk/common/src/main/java/io/opentelemetry/sdk/common/export/GrpcSenderConfig.java Adds configurable max response body size (default 4 MiB).
sdk/common/src/main/java/io/opentelemetry/sdk/common/export/GrpcSender.java Documents max-sized response message expectation.
sdk-extensions/jaeger-remote-sampler/.../JaegerRemoteSamplerBuilder.java Adds sampler-specific response body size setting and wires into sender config.
exporters/sender/okhttp/.../OkHttpHttpSuppressionTest.java Updates constructor usage with new max-size param.
exporters/sender/okhttp/.../OkHttpGrpcSuppressionTest.java Updates constructor usage with new max-size param.
exporters/sender/okhttp/.../OkHttpGrpcSenderTest.java Updates constructor usage with new max-size param.
exporters/sender/okhttp/.../OkHttpHttpSenderProvider.java Passes max response body size into sender.
exporters/sender/okhttp/.../OkHttpHttpSender.java Enforces bounded response reads + gzip handling.
exporters/sender/okhttp/.../OkHttpGrpcSenderProvider.java Passes max response body size into sender.
exporters/sender/okhttp/.../OkHttpGrpcSender.java Enforces bounded response reads + gzip handling for gRPC framing.
exporters/sender/okhttp/.../ImmutableHttpResponse.java Introduces AutoValue HTTP response wrapper for OkHttp sender.
exporters/sender/okhttp/.../ImmutableGrpcResponse.java Introduces AutoValue gRPC response wrapper for OkHttp sender.
exporters/sender/okhttp/build.gradle.kts Adds AutoValue annotation processor.
exporters/sender/jdk/.../JdkHttpSenderTest.java Updates constructor usage with new max-size param.
exporters/sender/jdk/.../JdkHttpSenderProvider.java Passes max response body size into sender.
exporters/sender/jdk/.../JdkHttpSender.java Enforces bounded response reads + gzip handling using JDK HttpClient.
exporters/sender/jdk/.../ImmutableHttpResponse.java Introduces AutoValue HTTP response wrapper for JDK sender.
exporters/sender/jdk/build.gradle.kts Adds AutoValue annotation processor.
exporters/sender/grpc-managed-channel/.../UpstreamGrpcSender.java Minor import cleanup; sender is relevant for new response-bound feature.
exporters/sender/grpc-managed-channel/.../ImmutableGrpcResponse.java Moves/adjusts internal ImmutableGrpcResponse type.
exporters/sender/grpc-managed-channel/build.gradle.kts Adds AutoValue annotation processor.
exporters/otlp/testing-internal/.../TelemetryExporter.java Adds ability for tests to generate response messages of a minimum size.
exporters/otlp/testing-internal/.../ManagedChannelTelemetryExporterBuilder.java Sets max inbound message size and forwards new test helper method.
exporters/otlp/testing-internal/.../AbstractHttpTelemetryExporterTest.java Adds HTTP response-bound and content-encoding tests.
exporters/otlp/testing-internal/.../AbstractGrpcTelemetryExporterTest.java Adds gRPC response-bound and encoding tests.
exporters/otlp/profiles/.../OtlpGrpcProfileExporterTest.java Updates test helper naming (addGrpcResponse).
exporters/otlp/all/.../OltpExporterBenchmark.java Updates OkHttp sender construction for new max-size parameter.
exporters/common/src/test/.../GrpcExporterTest.java Updates test to avoid depending on internal ImmutableGrpcResponse.
exporters/common/.../ImmutableHttpSenderConfig.java Wires max response body size into HttpSenderConfig AutoValue config.
exporters/common/.../HttpExporterBuilder.java Hardcodes 4 MiB bound for OTLP HTTP exporters for now.
exporters/common/.../ImmutableGrpcSenderConfig.java Wires max response body size into GrpcSenderConfig AutoValue config.
exporters/common/.../GrpcExporterBuilder.java Hardcodes 4 MiB bound for OTLP gRPC exporters for now.
docs/apidiffs/.../opentelemetry-sdk-extension-jaeger-remote-sampler.txt Records new Jaeger builder API.
docs/apidiffs/.../opentelemetry-sdk-common.txt Records new sender config APIs.
Comments suppressed due to low confidence (1)

exporters/sender/grpc-managed-channel/src/main/java/io/opentelemetry/exporter/sender/grpc/managedchannel/internal/ImmutableGrpcResponse.java:23

  • @AutoValue requires accessor methods for all properties. This class only declares getResponseMessage(), but GrpcResponse also requires getStatusCode() and getStatusDescription(). As written, AutoValue will not generate a type compatible with the create(statusCode, statusDescription, responseMessage) factory (compile error). Add abstract accessors for status code and description (and override them) so AutoValue can generate the correct implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

static ImmutableHttpResponse create(int statusCode, String statusMessage, byte[] responseBody) {
return new AutoValue_ImmutableHttpResponse(statusCode, statusMessage, responseBody);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

@AutoValue requires accessor methods for all properties. This class only declares getResponseBody(), but HttpResponse also requires getStatusCode() and getStatusMessage(). As written, the generated AutoValue_ImmutableHttpResponse will not match the create(statusCode, statusMessage, responseBody) factory and this will fail to compile. Add abstract accessors for statusCode and statusMessage (and annotate/override them) so AutoValue can generate the correct implementation.

Suggested change
@Override
public abstract int getStatusCode();
@Override
public abstract String getStatusMessage();

Copilot uses AI. Check for mistakes.
GrpcStatusCode statusCode, @Nullable String statusDescription, byte[] responseMessage) {
return new AutoValue_ImmutableGrpcResponse(statusCode, statusDescription, responseMessage);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

@AutoValue requires accessor methods for all properties. This class only declares getResponseMessage(), but GrpcResponse also requires getStatusCode() and getStatusDescription(). The generated AutoValue_ImmutableGrpcResponse therefore won’t match the create(statusCode, statusDescription, responseMessage) factory and will fail compilation. Add abstract accessors for status code and description (and override them) so AutoValue can generate the intended value type.

Suggested change
@Override
public abstract GrpcStatusCode getStatusCode();
@Nullable
@Override
public abstract String getStatusDescription();

Copilot uses AI. Check for mistakes.
static ImmutableHttpResponse create(int statusCode, String statusMessage, byte[] responseBody) {
return new AutoValue_ImmutableHttpResponse(statusCode, statusMessage, responseBody);
}

Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

@AutoValue requires accessor methods for all properties. This class only declares getResponseBody(), but HttpResponse also requires getStatusCode() and getStatusMessage(). With the current definition, the generated AutoValue_ImmutableHttpResponse won’t have the expected constructor signature used by create(...), causing a compile error. Add abstract accessors for status code and status message (and override them) so AutoValue can generate the correct implementation.

Suggested change
@Override
public abstract int getStatusCode();
@Override
public abstract String getStatusMessage();

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +190
long readUpTo =
maxResponseBodySize == Long.MAX_VALUE ? Long.MAX_VALUE : maxResponseBodySize + 1;
Buffer wireBuffer = new Buffer();
try {
while (wireBuffer.size() <= maxResponseBodySize) {
long n = body.source().read(wireBuffer, readUpTo - wireBuffer.size());
if (n == -1L) {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The response-size limit is currently applied to the wire buffer size (which includes the 5-byte gRPC frame header). This effectively reduces the allowed unframed message size by 5 bytes and can incorrectly fail responses whose protobuf message is exactly maxResponseBodySize. Consider allowing an extra 5 bytes (plus the overflow byte) for the frame header when bounding wireBuffer, and enforce maxResponseBodySize on the unframed/decompressed bodyBytes instead.

Copilot uses AI. Check for mistakes.
Comment on lines 20 to 26
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.ClientCalls;
import io.grpc.stub.MetadataUtils;
import io.opentelemetry.exporter.internal.grpc.ImmutableGrpcResponse;
import io.opentelemetry.exporter.internal.grpc.MarshalerInputStream;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.common.export.GrpcResponse;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This sender still reads the entire gRPC response into memory with no size bound (RESPONSE_MARSHALER.parse loops until EOF). With GrpcSenderConfig#getMaxResponseBodySize() added in this PR, UpstreamGrpcSender should enforce that limit (and map overflow to RESOURCE_EXHAUSTED per the spec) to avoid unbounded heap allocation.

Copilot uses AI. Check for mistakes.
Comment on lines +593 to +606
// We have a 4mb hardcoded response body limit. Responses <= 4mb succeed. Responses >= 4mb
// succeed. We can't test payloads exactly at 4mb because protobuf message lengths are finicky -
// its hard to create a message with an exact size.

// Body below the limit, succeeds
addHttpResponse(200, exporter.exportResponse(100));
CompletableResultCode result =
exporter
.export(Collections.singletonList(generateFakeTelemetry()))
.join(10, TimeUnit.SECONDS);
assertThat(result.isSuccess()).isTrue();

// Body over the limit, fails with RESOURCE_EXHAUSTED
addHttpResponse(200, exporter.exportResponse(4 * 1024 * 1024 + 1));
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This comment is internally inconsistent: it says responses <= 4mb succeed and responses >= 4mb succeed. Also, for HTTP the failure mode here is an exception, not RESOURCE_EXHAUSTED. Please correct the comment to reflect the intended boundary behavior and terminology so the test remains clear.

Copilot uses AI. Check for mistakes.
Comment on lines +371 to +384
// We have a 4mb hardcoded response body limit. Responses <= 4mb succeed. Responses >= 4mb
// succeed. We can't test payloads exactly at 4mb because protobuf message lengths are finicky -
// its hard to create a message with an exact size.

// Body below the limit, succeeds
addGrpcResponse(GrpcStatusCode.OK, null, exporter.exportResponse(100));
CompletableResultCode result =
exporter
.export(Collections.singletonList(generateFakeTelemetry()))
.join(10, TimeUnit.SECONDS);
assertThat(result.isSuccess()).isTrue();

// Body over the limit, fails with RESOURCE_EXHAUSTED
addGrpcResponse(GrpcStatusCode.OK, null, exporter.exportResponse(4 * 1024 * 1024 + 1));
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

This comment is internally inconsistent: it says responses <= 4mb succeed and responses >= 4mb succeed. Please correct it to reflect the intended boundary behavior (i.e., over-the-limit should fail) so the test documents the expected behavior accurately.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants