Skip to content

Commit c5db091

Browse files
fix: Sentry capturing compressed bodies when RequestDecompression middleware is enabled (#4315)
Resolves #4312: - #4312 #skip-changelog
1 parent baa279b commit c5db091

File tree

8 files changed

+447
-9
lines changed

8 files changed

+447
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
### Fixes
1414

15+
- Sentry now decompresses Request bodies in ASP.NET Core when RequestDecompression middleware is enabled ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315))
1516
- Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318))
1617
- Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314))
1718
- Native AOT: link to static `lzma` on Linux/MUSL ([#4326](https://github.com/getsentry/sentry-dotnet/pull/4326))
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
The code in this subdirectory has been adapted from
2+
https://github.com/dotnet/aspnetcore
3+
4+
The original license is as follows:
5+
6+
The MIT License (MIT)
7+
8+
Copyright (c) .NET Foundation and Contributors
9+
10+
All rights reserved.
11+
12+
Permission is hereby granted, free of charge, to any person obtaining a copy
13+
of this software and associated documentation files (the "Software"), to deal
14+
in the Software without restriction, including without limitation the rights
15+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16+
copies of the Software, and to permit persons to whom the Software is
17+
furnished to do so, subject to the following conditions:
18+
19+
The above copyright notice and this permission notice shall be included in all
20+
copies or substantial portions of the Software.
21+
22+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28+
SOFTWARE.
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Adapted from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs
2+
3+
// // Licensed to the .NET Foundation under one or more agreements.
4+
// The .NET Foundation licenses this file to you under the MIT license.
5+
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Http.Features;
8+
using Microsoft.AspNetCore.Http.Metadata;
9+
using Microsoft.AspNetCore.RequestDecompression;
10+
using Microsoft.Extensions.Logging;
11+
12+
namespace Sentry.AspNetCore.RequestDecompression;
13+
14+
/// <summary>
15+
/// Enables HTTP request decompression.
16+
/// </summary>
17+
internal sealed partial class RequestDecompressionMiddleware
18+
{
19+
private readonly RequestDelegate _next;
20+
private readonly ILogger<RequestDecompressionMiddleware> _logger;
21+
private readonly IRequestDecompressionProvider _provider;
22+
private readonly IHub _hub;
23+
24+
/// <summary>
25+
/// Initialize the request decompression middleware.
26+
/// </summary>
27+
/// <param name="next">The delegate representing the remaining middleware in the request pipeline.</param>
28+
/// <param name="logger">The logger.</param>
29+
/// <param name="provider">The <see cref="IRequestDecompressionProvider"/>.</param>
30+
/// <param name="hub">The Sentry Hub</param>
31+
public RequestDecompressionMiddleware(
32+
RequestDelegate next,
33+
ILogger<RequestDecompressionMiddleware> logger,
34+
IRequestDecompressionProvider provider,
35+
IHub hub)
36+
{
37+
ArgumentNullException.ThrowIfNull(next);
38+
ArgumentNullException.ThrowIfNull(logger);
39+
ArgumentNullException.ThrowIfNull(provider);
40+
ArgumentNullException.ThrowIfNull(hub);
41+
42+
_next = next;
43+
_logger = logger;
44+
_provider = provider;
45+
_hub = hub;
46+
}
47+
48+
/// <summary>
49+
/// Invoke the middleware.
50+
/// </summary>
51+
/// <param name="context">The <see cref="HttpContext"/>.</param>
52+
/// <returns>A task that represents the execution of this middleware.</returns>
53+
public Task Invoke(HttpContext context)
54+
{
55+
Stream? decompressionStream = null;
56+
try
57+
{
58+
decompressionStream = _provider.GetDecompressionStream(context);
59+
}
60+
catch (Exception e)
61+
{
62+
HandleException(e);
63+
}
64+
return decompressionStream is null
65+
? _next(context)
66+
: InvokeCore(context, decompressionStream);
67+
}
68+
69+
private async Task InvokeCore(HttpContext context, Stream decompressionStream)
70+
{
71+
var request = context.Request.Body;
72+
try
73+
{
74+
try
75+
{
76+
var sizeLimit =
77+
context.GetEndpoint()?.Metadata?.GetMetadata<IRequestSizeLimitMetadata>()?.MaxRequestBodySize
78+
?? context.Features.Get<IHttpMaxRequestBodySizeFeature>()?.MaxRequestBodySize;
79+
80+
context.Request.Body = new SizeLimitedStream(decompressionStream, sizeLimit, static (long sizeLimit) => throw new BadHttpRequestException(
81+
$"The decompressed request body is larger than the request body size limit {sizeLimit}.",
82+
StatusCodes.Status413PayloadTooLarge));
83+
}
84+
catch (Exception e)
85+
{
86+
HandleException(e);
87+
}
88+
89+
await _next(context).ConfigureAwait(false);
90+
}
91+
finally
92+
{
93+
context.Request.Body = request;
94+
await decompressionStream.DisposeAsync().ConfigureAwait(false);
95+
}
96+
}
97+
98+
private void HandleException(Exception e)
99+
{
100+
const string description =
101+
"An exception was captured and then re-thrown, when attempting to decompress the request body." +
102+
"The web server likely returned a 5xx error code as a result of this exception.";
103+
e.SetSentryMechanism(nameof(RequestDecompressionMiddleware), description, handled: false);
104+
_hub.CaptureException(e);
105+
ExceptionDispatchInfo.Capture(e).Throw();
106+
}
107+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copied from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Shared/SizeLimitedStream.cs
2+
// The only changes are the namespace and the addition of this comment
3+
4+
// Licensed to the .NET Foundation under one or more agreements.
5+
// The .NET Foundation licenses this file to you under the MIT license.
6+
7+
namespace Sentry.AspNetCore.RequestDecompression;
8+
9+
#nullable enable
10+
11+
internal sealed class SizeLimitedStream : Stream
12+
{
13+
private readonly Stream _innerStream;
14+
private readonly long? _sizeLimit;
15+
private readonly Action<long>? _handleSizeLimit;
16+
private long _totalBytesRead;
17+
18+
public SizeLimitedStream(Stream innerStream, long? sizeLimit, Action<long>? handleSizeLimit = null)
19+
{
20+
ArgumentNullException.ThrowIfNull(innerStream);
21+
22+
_innerStream = innerStream;
23+
_sizeLimit = sizeLimit;
24+
_handleSizeLimit = handleSizeLimit;
25+
}
26+
27+
public override bool CanRead => _innerStream.CanRead;
28+
29+
public override bool CanSeek => _innerStream.CanSeek;
30+
31+
public override bool CanWrite => _innerStream.CanWrite;
32+
33+
public override long Length => _innerStream.Length;
34+
35+
public override long Position
36+
{
37+
get
38+
{
39+
return _innerStream.Position;
40+
}
41+
set
42+
{
43+
_innerStream.Position = value;
44+
}
45+
}
46+
47+
public override void Flush()
48+
{
49+
_innerStream.Flush();
50+
}
51+
52+
public override int Read(byte[] buffer, int offset, int count)
53+
{
54+
var bytesRead = _innerStream.Read(buffer, offset, count);
55+
56+
_totalBytesRead += bytesRead;
57+
if (_totalBytesRead > _sizeLimit)
58+
{
59+
if (_handleSizeLimit != null)
60+
{
61+
_handleSizeLimit(_sizeLimit.Value);
62+
}
63+
else
64+
{
65+
throw new InvalidOperationException("The maximum number of bytes have been read.");
66+
}
67+
}
68+
69+
return bytesRead;
70+
}
71+
72+
public override long Seek(long offset, SeekOrigin origin)
73+
{
74+
return _innerStream.Seek(offset, origin);
75+
}
76+
77+
public override void SetLength(long value)
78+
{
79+
_innerStream.SetLength(value);
80+
}
81+
82+
public override void Write(byte[] buffer, int offset, int count)
83+
{
84+
_innerStream.Write(buffer, offset, count);
85+
}
86+
87+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
88+
{
89+
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
90+
}
91+
92+
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
93+
{
94+
#pragma warning disable CA2007
95+
var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken);
96+
#pragma warning restore CA2007
97+
98+
_totalBytesRead += bytesRead;
99+
if (_totalBytesRead > _sizeLimit)
100+
{
101+
if (_handleSizeLimit != null)
102+
{
103+
_handleSizeLimit(_sizeLimit.Value);
104+
}
105+
else
106+
{
107+
throw new InvalidOperationException("The maximum number of bytes have been read.");
108+
}
109+
}
110+
111+
return bytesRead;
112+
}
113+
}

src/Sentry.AspNetCore/SentryStartupFilter.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
using Microsoft.AspNetCore.Builder;
22
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.RequestDecompression;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Options;
6+
using Sentry.AspNetCore.RequestDecompression;
7+
using Sentry.Extensibility;
38

49
namespace Sentry.AspNetCore;
510

@@ -11,10 +16,19 @@ public class SentryStartupFilter : IStartupFilter
1116
/// <summary>
1217
/// Adds Sentry to the pipeline.
1318
/// </summary>
14-
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => e =>
19+
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
1520
{
16-
e.UseSentry();
21+
// If we are capturing request bodies and the user has configured request body decompression, we need to
22+
// ensure that the RequestDecompression middleware gets called before Sentry's middleware.
23+
var options = app.ApplicationServices.GetService<IOptions<SentryAspNetCoreOptions>>();
24+
if (options?.Value is { } o && o.MaxRequestBodySize != RequestSize.None
25+
&& app.ApplicationServices.GetService<IRequestDecompressionProvider>() is not null)
26+
{
27+
app.UseMiddleware<RequestDecompressionMiddleware>();
28+
}
1729

18-
next(e);
30+
app.UseSentry();
31+
32+
next(app);
1933
};
2034
}

src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ public abstract class BaseRequestPayloadExtractor : IRequestPayloadExtractor
1818
return null;
1919
}
2020

21-
if (request.Body == null
22-
|| !request.Body.CanSeek
23-
|| !request.Body.CanRead
24-
|| !IsSupported(request))
21+
if (request.Body is not { CanRead: true } || !IsSupported(request))
2522
{
2623
return null;
2724
}
2825

26+
if (!request.Body.CanSeek)
27+
{
28+
// When RequestDecompression is enabled, the RequestDecompressionMiddleware will store a SizeLimitedStream
29+
// in the request body after decompression. Seek operations throw an exception, but we can still read the stream
30+
return DoExtractPayLoad(request);
31+
}
32+
2933
var originalPosition = request.Body.Position;
3034
try
3135
{
3236
request.Body.Position = 0;
33-
3437
return DoExtractPayLoad(request);
3538
}
3639
finally

test/Sentry.AspNetCore.TestUtils/FakeSentryServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace Sentry.AspNetCore.TestUtils;
77

8-
internal static class FakeSentryServer
8+
public static class FakeSentryServer
99
{
1010
public static TestServer CreateServer(IReadOnlyCollection<RequestHandler> handlers)
1111
{

0 commit comments

Comments
 (0)