Skip to content

Commit 77dc5d1

Browse files
[OneCollector] Limit response body size read (#4117)
Co-authored-by: Rajkumar Rangaraj <rajrang@microsoft.com>
1 parent 1718625 commit 77dc5d1

7 files changed

Lines changed: 651 additions & 13 deletions

File tree

src/OpenTelemetry.Exporter.OneCollector/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* Updated OpenTelemetry core component version(s) to `1.15.2`.
66
([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080))
77

8+
* Limit how much of the response body is read when export fails using the HTTP
9+
JSON transport and informational logging is enabled.
10+
([#4117](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4117))
11+
812
## 1.15.0
913

1014
Released 2026-Jan-21

src/OpenTelemetry.Exporter.OneCollector/Internal/Transports/HttpJsonPostTransport.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ public bool Send(in TransportSendRequest sendRequest)
103103
request.Headers.TryAddWithoutValidation("NoResponseBody", "true");
104104
}
105105

106-
using var response = this.httpClient.Send(
107-
request,
108-
infoLoggingEnabled ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
109-
CancellationToken.None);
106+
var cancellationToken = CancellationToken.None;
107+
var completionOption = HttpCompletionOption.ResponseHeadersRead;
108+
109+
using var response = this.httpClient.Send(request, completionOption, cancellationToken);
110110

111111
if (response.IsSuccessStatusCode)
112112
{
@@ -130,11 +130,14 @@ public bool Send(in TransportSendRequest sendRequest)
130130
}
131131
else
132132
{
133-
response.Headers.TryGetValues("Collector-Error", out var collectorErrors);
133+
_ = response.Headers.TryGetValues("Collector-Error", out var collectorErrors);
134+
135+
string? errorDetails = null;
134136

135-
var errorDetails = infoLoggingEnabled
136-
? response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
137-
: null;
137+
if (infoLoggingEnabled)
138+
{
139+
errorDetails = HttpClientHelpers.TryGetResponseBodyAsString(response, cancellationToken);
140+
}
138141

139142
OneCollectorExporterEventSource.Log.WriteHttpTransportErrorResponseReceivedEventIfEnabled(
140143
this.Description,

src/OpenTelemetry.Exporter.OneCollector/Internal/Transports/IHttpClient.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,12 @@ public HttpClientWrapper(HttpClient httpClient)
3838
public HttpResponseMessage Send(
3939
HttpRequestMessage request,
4040
HttpCompletionOption completionOption,
41-
CancellationToken cancellationToken)
42-
{
41+
CancellationToken cancellationToken) =>
4342
#if NET
44-
return this.synchronousSendSupportedByCurrentPlatform
43+
this.synchronousSendSupportedByCurrentPlatform
4544
? this.httpClient.Send(request, completionOption, cancellationToken)
4645
: this.httpClient.SendAsync(request, completionOption, cancellationToken).GetAwaiter().GetResult();
4746
#else
48-
return this.httpClient.SendAsync(request, completionOption, cancellationToken).GetAwaiter().GetResult();
47+
this.httpClient.SendAsync(request, completionOption, cancellationToken).GetAwaiter().GetResult();
4948
#endif
50-
}
5149
}

src/OpenTelemetry.Exporter.OneCollector/OpenTelemetry.Exporter.OneCollector.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<ItemGroup>
4242
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
4343
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
44+
<Compile Include="$(RepoRoot)\src\Shared\HttpClientHelpers.cs" Link="Includes\HttpClientHelpers.cs" />
4445
<Compile Include="$(RepoRoot)\src\Shared\IsExternalInit.cs" Link="Includes\IsExternalInit.cs" />
4546
<Compile Include="$(RepoRoot)\src\Shared\Lock.cs" Link="Includes\Lock.cs" />
4647
</ItemGroup>

src/Shared/HttpClientHelpers.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
#if NET
5+
using System.Buffers;
6+
#endif
7+
using System.Text;
8+
9+
namespace System.Net.Http;
10+
11+
internal static class HttpClientHelpers
12+
{
13+
private const int DefaultMessageSizeLimit = 4 * 1024 * 1024; // 4MiB
14+
15+
internal static string? TryGetResponseBodyAsString(HttpResponseMessage? httpResponse, CancellationToken cancellationToken)
16+
=> GetResponseBodyAsString(allowTruncation: true, DefaultMessageSizeLimit, httpResponse, cancellationToken);
17+
18+
internal static string? GetResponseBodyAsString(HttpResponseMessage? httpResponse, CancellationToken cancellationToken)
19+
=> GetResponseBodyAsString(allowTruncation: false, DefaultMessageSizeLimit, httpResponse, cancellationToken);
20+
21+
private static string? GetResponseBodyAsString(
22+
bool allowTruncation,
23+
int limit,
24+
HttpResponseMessage? httpResponse,
25+
CancellationToken cancellationToken)
26+
{
27+
if (httpResponse?.Content is null)
28+
{
29+
return null;
30+
}
31+
32+
if (cancellationToken.IsCancellationRequested)
33+
{
34+
if (allowTruncation)
35+
{
36+
return null;
37+
}
38+
39+
cancellationToken.ThrowIfCancellationRequested();
40+
}
41+
42+
try
43+
{
44+
#if NET
45+
var stream = httpResponse.Content.ReadAsStream(cancellationToken);
46+
#else
47+
var stream = httpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
48+
#endif
49+
50+
var length = GetBufferLength(stream, limit);
51+
52+
#if NET
53+
var buffer = ArrayPool<byte>.Shared.Rent(length);
54+
#else
55+
var buffer = new byte[length];
56+
#endif
57+
58+
try
59+
{
60+
var totalRead = 0;
61+
62+
// Read raw bytes so the size limit applies to bytes rather than characters
63+
while (totalRead < limit && !cancellationToken.IsCancellationRequested)
64+
{
65+
var bytesRead = stream.Read(buffer, totalRead, length - totalRead);
66+
67+
if (bytesRead is 0)
68+
{
69+
break;
70+
}
71+
72+
totalRead += bytesRead;
73+
}
74+
75+
// We've read exactly limit bytes. Check if there's more data.
76+
var probe = new byte[1];
77+
78+
#if NETFRAMEWORK || NETSTANDARD
79+
var extra = stream.Read(probe, 0, 1);
80+
#else
81+
var extra = stream.Read(probe);
82+
#endif
83+
84+
if (extra > 0 && !allowTruncation)
85+
{
86+
// + 1: we read exactly MaxMessageSize bytes and confirmed at least one more byte exists.
87+
throw new InvalidOperationException($"Response body exceeded the size limit of {limit} bytes.");
88+
}
89+
90+
if (!allowTruncation)
91+
{
92+
cancellationToken.ThrowIfCancellationRequested();
93+
}
94+
95+
// Decode using the charset from the response content headers, if available
96+
var encoding = GetEncoding(httpResponse.Content.Headers.ContentType?.CharSet);
97+
var result = encoding.GetString(buffer, 0, totalRead);
98+
99+
if (extra > 0)
100+
{
101+
result += "[TRUNCATED]";
102+
}
103+
104+
return result;
105+
}
106+
finally
107+
{
108+
#if NET
109+
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
110+
#endif
111+
}
112+
}
113+
catch (Exception) when (allowTruncation)
114+
{
115+
return null;
116+
}
117+
}
118+
119+
private static int GetBufferLength(Stream stream, int limit)
120+
{
121+
try
122+
{
123+
// Avoid allocating an overly large buffer if the stream is smaller than the size limit
124+
return stream.Length < limit ? (int)stream.Length : limit;
125+
}
126+
catch (Exception)
127+
{
128+
// Not all Stream types support Length, so default to the maximum
129+
return limit;
130+
}
131+
}
132+
133+
private static Encoding GetEncoding(string? name)
134+
{
135+
Encoding encoding = Encoding.UTF8;
136+
137+
if (!string.IsNullOrWhiteSpace(name))
138+
{
139+
try
140+
{
141+
encoding = Encoding.GetEncoding(name);
142+
}
143+
catch (Exception)
144+
{
145+
// Invalid encoding name
146+
}
147+
}
148+
149+
return encoding;
150+
}
151+
}

0 commit comments

Comments
 (0)