Skip to content

feat: limit OTLP HTTP response body to 4 MiB#1935

Open
bobstrecansky wants to merge 1 commit intomainfrom
fix/response-body-size-limit
Open

feat: limit OTLP HTTP response body to 4 MiB#1935
bobstrecansky wants to merge 1 commit intomainfrom
fix/response-body-size-limit

Conversation

@bobstrecansky
Copy link
Copy Markdown
Contributor

Prevents unbounded memory usage when a misconfigured or malicious collector sends an oversized response body.

  • Add ResponseBodySizeLimit::MAX_BYTES constant (4 MiB)
  • Add PsrUtils::readBodyWithSizeLimit() and decode()
  • Update PsrTransport::handleResponse() to use the size-capped reader
  • Add TransportResponseException for typed HTTP error handling
  • Add ResponseBodySizeLimitTest (10 test cases)

Fixes #1932

Prevents unbounded memory usage when a misconfigured or malicious
collector sends an oversized response body.

- Add ResponseBodySizeLimit::MAX_BYTES constant (4 MiB)
- Add PsrUtils::readBodyWithSizeLimit() and decode()
- Update PsrTransport::handleResponse() to use the size-capped reader
- Add TransportResponseException for typed HTTP error handling
- Add ResponseBodySizeLimitTest (10 test cases)
Fixes #1932
@bobstrecansky bobstrecansky requested a review from a team as a code owner April 9, 2026 13:43
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 30.98592% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.10%. Comparing base (6fef63f) to head (c78e578).

Files with missing lines Patch % Lines
src/SDK/Common/Export/Http/PsrTransport.php 0.00% 38 Missing ⚠️
.../Common/Export/Http/TransportResponseException.php 0.00% 8 Missing ⚠️
src/SDK/Common/Export/Http/PsrUtils.php 88.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #1935      +/-   ##
============================================
- Coverage     68.17%   66.10%   -2.08%     
+ Complexity     3021     2999      -22     
============================================
  Files           450      451       +1     
  Lines          8820     8771      -49     
============================================
- Hits           6013     5798     -215     
- Misses         2807     2973     +166     
Flag Coverage Δ
8.1 ?
8.2 ?
8.3 ?
8.4 ?
8.5 66.10% <30.98%> (-1.22%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/SDK/Common/Export/Http/PsrUtils.php 88.00% <88.00%> (-4.65%) ⬇️
.../Common/Export/Http/TransportResponseException.php 0.00% <0.00%> (ø)
src/SDK/Common/Export/Http/PsrTransport.php 0.00% <0.00%> (-94.45%) ⬇️

... and 12 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6fef63f...c78e578. Read the comment docs.

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

{
$body = $payload;

if ($this->compression === 'gzip') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ($this->compression === 'gzip') {
if ($this->compression === 'gzip' && extension_loaded('zlib')) {

Same for the other if ($this->compression === 'gzip') below; can we merge these two ifs?

Alternatively check in constructor.

string $endpoint,
string $contentType,
array $headers = [],
string $compression = 'none'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
string $compression = 'none'
?string $compression = null,

->withBody($stream)
->withHeader('Content-Type', $this->contentType);

for ($retries = 0;; $retries++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is the removal of retries intended?

continue;
}
// (3) Read at most $maxRead bytes.
$body = $response->getBody()->read($maxRead);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Has to call read in a loop.

Fewer than $length bytes may be returned if underlying stream call returns fewer bytes.

if ($value === '') {
return $value;
}
$body = self::readBodyWithSizeLimit($response);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should decompress the response body stream while reading it instead of buffering its content and then decompressing the entire body at once.

if ($result === false) {
throw new LogicException();
}
return $decoded;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The limit must apply to the decompressed body, see OTLP/HTTP Response:

The client MUST limit the size of the response body when parsing it, including after decompression, to mitigate possible excessive memory usage caused by a misconfigured or malicious server.

Comment on lines -31 to +29
final class PsrTransport implements TransportInterface
final class PsrTransport
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This change seems unintended?

private readonly array $compression,
private readonly int $retryDelay,
private readonly int $maxRetries,
ClientInterface $client,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why has the constructor property promotion been removed here? 🤔

Comment on lines +17 to +26
{
private int $statusCode;
private string $responseBody;

public function __construct(int $statusCode, string $responseBody, string $message = '')
{
parent::__construct($message !== '' ? $message : sprintf('HTTP %d', $statusCode));
$this->statusCode = $statusCode;
$this->responseBody = $responseBody;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
{
private int $statusCode;
private string $responseBody;
public function __construct(int $statusCode, string $responseBody, string $message = '')
{
parent::__construct($message !== '' ? $message : sprintf('HTTP %d', $statusCode));
$this->statusCode = $statusCode;
$this->responseBody = $responseBody;
}
{
public function __construct(
private int $statusCode,
private string $responseBody,
string $message = '',
) {
parent::__construct($message ?: sprintf('HTTP %d', $statusCode));
}

bobstrecansky added a commit to bobstrecansky/opentelemetry-php that referenced this pull request Apr 14, 2026
- Restore PsrTransport to original structure (implements TransportInterface,
  constructor property promotion, retry logic, closed state, shutdown/forceFlush)
- Change PsrUtils::decode() to accept ResponseInterface instead of string,
  applying the 4 MiB size limit to the **decompressed** body per OTLP spec
- Add readWithLimit() that loops on stream read() for partial returns
- Remove unused TransportResponseException (no longer referenced)
- Update PsrUtilsTest to match new decode() signature
- Update ResponseBodySizeLimitTest for new behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

Response body size limitation

3 participants