From 1e9d22bed0767cbdd1c32d652edf1e7f964fc75c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 30 Jun 2025 10:28:17 +1200 Subject: [PATCH 01/16] Fix Directory.Build.targets when using SentryNoMobile.slnf #skip-changelog --- samples/Directory.Build.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/Directory.Build.targets b/samples/Directory.Build.targets index d8a727cf4f..e757f55436 100644 --- a/samples/Directory.Build.targets +++ b/samples/Directory.Build.targets @@ -10,7 +10,7 @@ + Condition="'$(SENTRY_DSN)' != ''"> @@ -25,7 +25,7 @@ namespace Sentry.Samples%3B internal static class EnvironmentVariables { /// <summary> - /// To make things easier for the SDK maintainers we have a custom build target that writes the + /// To make things easier for the SDK maintainers, we have a custom build target that writes the /// SENTRY_DSN environment variable into an EnvironmentVariables class that is available for mobile /// targets. This allows us to share one DSN defined in the ENV across desktop and mobile samples. /// Generally, you won't want to do this in your own mobile projects though - you should set the DSN From 75dec327c72649a34ccc708dac1c00d0108d6c42 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 30 Jun 2025 16:45:19 +1200 Subject: [PATCH 02/16] fix: Sentry capturing compressed bodies when RequestDecompression middleware is enabled Resolves #4312: - https://github.com/getsentry/sentry-dotnet/issues/4312 --- src/Sentry.AspNetCore/SentryStartupFilter.cs | 20 ++++++++++++++++--- .../BaseRequestPayloadExtractor.cs | 19 +++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index 34fa0b94e0..e2831597b3 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -1,5 +1,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.RequestDecompression; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Sentry.Extensibility; namespace Sentry.AspNetCore; @@ -11,10 +15,20 @@ public class SentryStartupFilter : IStartupFilter /// /// Adds Sentry to the pipeline. /// - public Action Configure(Action next) => e => + public Action Configure(Action next) => app => { - e.UseSentry(); + app.UseSentry(); - next(e); + // If we are capturing request bodies and the user has configured request body decompression, we need to + // ensure that the RequestDecompression middleware gets called before Sentry's middleware. The last middleware + // added is the first one to be executed. + var options = app.ApplicationServices.GetService>(); + if (options?.Value is { } o&& o.MaxRequestBodySize != RequestSize.None + && app.ApplicationServices.GetService() is not null) + { + app.UseRequestDecompression(); + } + + next(app); }; } diff --git a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs index 2b10c67d1b..7efedc3f44 100644 --- a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs +++ b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs @@ -18,24 +18,29 @@ public abstract class BaseRequestPayloadExtractor : IRequestPayloadExtractor return null; } - if (request.Body == null - || !request.Body.CanSeek - || !request.Body.CanRead - || !IsSupported(request)) + if (request.Body is not { CanRead: true } || !IsSupported(request)) { return null; } - var originalPosition = request.Body.Position; + // When RequestDecompression is enabled, the RequestDecompressionMiddleware will store a SizeLimitedStream + // in the request body after decompression. Seek operations throw an exception, but we can still read the stream + var originalPosition = request.Body.CanSeek ? request.Body.Position : 0; try { - request.Body.Position = 0; + if (request.Body.CanSeek) + { + request.Body.Position = 0; + } return DoExtractPayLoad(request); } finally { - request.Body.Position = originalPosition; + if (request.Body.CanSeek) + { + request.Body.Position = originalPosition; + } } } From 839fb3766f104dd927c3652148f3062759d2891e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 30 Jun 2025 05:06:28 +0000 Subject: [PATCH 03/16] Format code --- src/Sentry.AspNetCore/SentryStartupFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index e2831597b3..25bb1a0cc3 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -23,7 +23,7 @@ public Action Configure(Action next) = // ensure that the RequestDecompression middleware gets called before Sentry's middleware. The last middleware // added is the first one to be executed. var options = app.ApplicationServices.GetService>(); - if (options?.Value is { } o&& o.MaxRequestBodySize != RequestSize.None + if (options?.Value is { } o && o.MaxRequestBodySize != RequestSize.None && app.ApplicationServices.GetService() is not null) { app.UseRequestDecompression(); From f3873ada771ef7ab0379f299b8b4510d73d69f29 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 30 Jun 2025 20:49:02 +1200 Subject: [PATCH 04/16] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f325b27ee8..3910e77766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Added StartSpan and GetTransaction methods to the SentrySdk ([#4303](https://github.com/getsentry/sentry-dotnet/pull/4303)) +### Fixes + +- When Sentry is configured to capture Request bodies in ASP.NET Core, the uncompressed content is not captured when RequestDecompression middleware is enabled. Previously the request bodies were being captured prior to decompression ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) + ### Dependencies - Bump Native SDK from v0.9.0 to v0.9.1 ([#4309](https://github.com/getsentry/sentry-dotnet/pull/4309)) From 12406941e49e8d604358cdd4e41f807adbcd7829 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 3 Jul 2025 14:51:29 +1200 Subject: [PATCH 05/16] Vendored in the RequestDecompressionMiddleware so we can catch any exceptions in this --- .../RequestDecompression/ATTRIBUTION.txt | 28 +++++ .../RequestDecompressionMiddleware.cs | 107 +++++++++++++++++ .../RequestDecompression/SizeLimitedStream.cs | 112 ++++++++++++++++++ .../SentryAspNetCoreOptions.cs | 6 +- src/Sentry.AspNetCore/SentryStartupFilter.cs | 8 +- 5 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt create mode 100644 src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs create mode 100644 src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt b/src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt new file mode 100644 index 0000000000..ff233854f6 --- /dev/null +++ b/src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt @@ -0,0 +1,28 @@ +The code in this subdirectory has been adapted from +https://github.com/dotnet/aspnetcore + +The original license is as follows: + +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs b/src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs new file mode 100644 index 0000000000..4900f7b129 --- /dev/null +++ b/src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs @@ -0,0 +1,107 @@ +// Adapted from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Middleware/RequestDecompression/src/RequestDecompressionMiddleware.cs + +// // Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.RequestDecompression; +using Microsoft.Extensions.Logging; + +namespace Sentry.AspNetCore.Internal.RequestDecompression; + +/// +/// Enables HTTP request decompression. +/// +internal sealed partial class RequestDecompressionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private readonly IRequestDecompressionProvider _provider; + private readonly IHub _hub; + + /// + /// Initialize the request decompression middleware. + /// + /// The delegate representing the remaining middleware in the request pipeline. + /// The logger. + /// The . + /// The Sentry Hub + public RequestDecompressionMiddleware( + RequestDelegate next, + ILogger logger, + IRequestDecompressionProvider provider, + IHub hub) + { + ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(logger); + ArgumentNullException.ThrowIfNull(provider); + ArgumentNullException.ThrowIfNull(hub); + + _next = next; + _logger = logger; + _provider = provider; + _hub = hub; + } + + /// + /// Invoke the middleware. + /// + /// The . + /// A task that represents the execution of this middleware. + public Task Invoke(HttpContext context) + { + Stream? decompressionStream = null; + try + { + decompressionStream = _provider.GetDecompressionStream(context); + } + catch (Exception e) + { + HandleException(e); + } + return decompressionStream is null + ? _next(context) + : InvokeCore(context, decompressionStream); + } + + private async Task InvokeCore(HttpContext context, Stream decompressionStream) + { + var request = context.Request.Body; + try + { + try + { + var sizeLimit = + context.GetEndpoint()?.Metadata?.GetMetadata()?.MaxRequestBodySize + ?? context.Features.Get()?.MaxRequestBodySize; + + context.Request.Body = new SizeLimitedStream(decompressionStream, sizeLimit, static (long sizeLimit) => throw new BadHttpRequestException( + $"The decompressed request body is larger than the request body size limit {sizeLimit}.", + StatusCodes.Status413PayloadTooLarge)); + } + catch (Exception e) + { + HandleException(e); + } + + await _next(context).ConfigureAwait(false); + } + finally + { + context.Request.Body = request; + await decompressionStream.DisposeAsync().ConfigureAwait(false); + } + } + + private void HandleException(Exception e) + { + const string description = + "An exception was captured and then re-thrown, when attempting to decompress the request body." + + "The web server likely returned a 5xx error code as a result of this exception."; + e.SetSentryMechanism("RequestDecompressionMiddleware", description, handled: false); + _hub.CaptureException(e); + ExceptionDispatchInfo.Capture(e).Throw(); + } +} diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs b/src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs new file mode 100644 index 0000000000..3e7d88fb95 --- /dev/null +++ b/src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs @@ -0,0 +1,112 @@ +// Adapted from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Shared/SizeLimitedStream.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Sentry.AspNetCore.Internal.RequestDecompression; + +#nullable enable + +internal sealed class SizeLimitedStream : Stream +{ + private readonly Stream _innerStream; + private readonly long? _sizeLimit; + private readonly Action? _handleSizeLimit; + private long _totalBytesRead; + + public SizeLimitedStream(Stream innerStream, long? sizeLimit, Action? handleSizeLimit = null) + { + ArgumentNullException.ThrowIfNull(innerStream); + + _innerStream = innerStream; + _sizeLimit = sizeLimit; + _handleSizeLimit = handleSizeLimit; + } + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get + { + return _innerStream.Position; + } + set + { + _innerStream.Position = value; + } + } + + public override void Flush() + { + _innerStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesRead = _innerStream.Read(buffer, offset, count); + + _totalBytesRead += bytesRead; + if (_totalBytesRead > _sizeLimit) + { + if (_handleSizeLimit != null) + { + _handleSizeLimit(_sizeLimit.Value); + } + else + { + throw new InvalidOperationException("The maximum number of bytes have been read."); + } + } + + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _innerStream.Write(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { +#pragma warning disable CA2007 + var bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken); +#pragma warning restore CA2007 + + _totalBytesRead += bytesRead; + if (_totalBytesRead > _sizeLimit) + { + if (_handleSizeLimit != null) + { + _handleSizeLimit(_sizeLimit.Value); + } + else + { + throw new InvalidOperationException("The maximum number of bytes have been read."); + } + } + + return bytesRead; + } +} diff --git a/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs b/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs index 345a92d69e..e3f6494b4c 100644 --- a/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs +++ b/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs @@ -122,15 +122,15 @@ internal void SetEnvironment(IWebHostEnvironment hostingEnvironment) if (hostingEnvironment.IsProduction()) { - Environment = Internal.Constants.ProductionEnvironmentSetting; + Environment = Sentry.Internal.Constants.ProductionEnvironmentSetting; } else if (hostingEnvironment.IsStaging()) { - Environment = Internal.Constants.StagingEnvironmentSetting; + Environment = Sentry.Internal.Constants.StagingEnvironmentSetting; } else if (hostingEnvironment.IsDevelopment()) { - Environment = Internal.Constants.DevelopmentEnvironmentSetting; + Environment = Sentry.Internal.Constants.DevelopmentEnvironmentSetting; } else { diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index 25bb1a0cc3..b60f92854f 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -17,8 +17,6 @@ public class SentryStartupFilter : IStartupFilter /// public Action Configure(Action next) => app => { - app.UseSentry(); - // If we are capturing request bodies and the user has configured request body decompression, we need to // ensure that the RequestDecompression middleware gets called before Sentry's middleware. The last middleware // added is the first one to be executed. @@ -26,9 +24,13 @@ public Action Configure(Action next) = if (options?.Value is { } o && o.MaxRequestBodySize != RequestSize.None && app.ApplicationServices.GetService() is not null) { - app.UseRequestDecompression(); + // We've vendored in a custom implementation of RequestDecompressionMiddleware to ensure we can capture + // any exceptions that might occur during decompression. + app.UseMiddleware(); } + app.UseSentry(); + next(app); }; } From 2ef850d98ec40cc29979a639fdbf8c52d3c93ed9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 3 Jul 2025 15:02:05 +1200 Subject: [PATCH 06/16] . --- .../{Internal => }/RequestDecompression/ATTRIBUTION.txt | 0 .../RequestDecompressionMiddleware.cs | 2 +- .../RequestDecompression/SizeLimitedStream.cs | 5 +++-- src/Sentry.AspNetCore/SentryStartupFilter.cs | 8 +++----- 4 files changed, 7 insertions(+), 8 deletions(-) rename src/Sentry.AspNetCore/{Internal => }/RequestDecompression/ATTRIBUTION.txt (100%) rename src/Sentry.AspNetCore/{Internal => }/RequestDecompression/RequestDecompressionMiddleware.cs (98%) rename src/Sentry.AspNetCore/{Internal => }/RequestDecompression/SizeLimitedStream.cs (92%) diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt b/src/Sentry.AspNetCore/RequestDecompression/ATTRIBUTION.txt similarity index 100% rename from src/Sentry.AspNetCore/Internal/RequestDecompression/ATTRIBUTION.txt rename to src/Sentry.AspNetCore/RequestDecompression/ATTRIBUTION.txt diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs b/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs similarity index 98% rename from src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs rename to src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs index 4900f7b129..848397f13d 100644 --- a/src/Sentry.AspNetCore/Internal/RequestDecompression/RequestDecompressionMiddleware.cs +++ b/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.RequestDecompression; using Microsoft.Extensions.Logging; -namespace Sentry.AspNetCore.Internal.RequestDecompression; +namespace Sentry.AspNetCore.RequestDecompression; /// /// Enables HTTP request decompression. diff --git a/src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs b/src/Sentry.AspNetCore/RequestDecompression/SizeLimitedStream.cs similarity index 92% rename from src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs rename to src/Sentry.AspNetCore/RequestDecompression/SizeLimitedStream.cs index 3e7d88fb95..9264bbc555 100644 --- a/src/Sentry.AspNetCore/Internal/RequestDecompression/SizeLimitedStream.cs +++ b/src/Sentry.AspNetCore/RequestDecompression/SizeLimitedStream.cs @@ -1,9 +1,10 @@ -// Adapted from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Shared/SizeLimitedStream.cs +// Copied from: https://github.com/dotnet/aspnetcore/blob/c18e93a9a2e2949e1a9c880da16abf0837aa978f/src/Shared/SizeLimitedStream.cs +// The only changes are the namespace and the addition of this comment // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Sentry.AspNetCore.Internal.RequestDecompression; +namespace Sentry.AspNetCore.RequestDecompression; #nullable enable diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index b60f92854f..998e4bf560 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.RequestDecompression; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Sentry.AspNetCore.RequestDecompression; using Sentry.Extensibility; namespace Sentry.AspNetCore; @@ -18,15 +19,12 @@ public class SentryStartupFilter : IStartupFilter public Action Configure(Action next) => app => { // If we are capturing request bodies and the user has configured request body decompression, we need to - // ensure that the RequestDecompression middleware gets called before Sentry's middleware. The last middleware - // added is the first one to be executed. + // ensure that the RequestDecompression middleware gets called before Sentry's middleware. var options = app.ApplicationServices.GetService>(); if (options?.Value is { } o && o.MaxRequestBodySize != RequestSize.None && app.ApplicationServices.GetService() is not null) { - // We've vendored in a custom implementation of RequestDecompressionMiddleware to ensure we can capture - // any exceptions that might occur during decompression. - app.UseMiddleware(); + app.UseMiddleware(); } app.UseSentry(); From ee1b9b9c01572d23c90521df28214d773657a357 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 4 Jul 2025 14:59:43 +1200 Subject: [PATCH 07/16] Create RequestDecompressionMiddlewareTests.cs --- .../RequestDecompressionMiddlewareTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs new file mode 100644 index 0000000000..3e59c39650 --- /dev/null +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; + +namespace Sentry.AspNetCore.Tests.RequestDecompressionMiddleware; + +public class RequestDecompressionMiddlewareTests +{ + [Fact] + public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() + { + // Arrange + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddRouting(); + services.AddRequestDecompression(); // No options needed for default gzip support + }) + .UseSentry(o => + { + o.Dsn = ValidDsn; + o.MaxRequestBodySize = RequestSize.Always; + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapPost("/echo", async context => + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + var body = await reader.ReadToEndAsync(); + await context.Response.WriteAsync(body); + }); + }); + }); + + using var server = new TestServer(builder); + using var client = server.CreateClient(); + + var json = "{\"Foo\":\"Bar\"}"; + var gzipped = CompressGzip(json); + var content = new ByteArrayContent(gzipped); + content.Headers.Add("Content-Encoding", "gzip"); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + // Act + var response = await client.PostAsync("/echo", content); + var responseBody = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(json, responseBody); + } + + private static byte[] CompressGzip(string str) + { + var bytes = Encoding.UTF8.GetBytes(str); + using var output = new MemoryStream(); + using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true)) + { + gzip.Write(bytes, 0, bytes.Length); + } + return output.ToArray(); + } +} From a4ec827a885e373a183b287ba826d7bbcea3343c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 4 Jul 2025 15:16:59 +1200 Subject: [PATCH 08/16] Update RequestDecompressionMiddlewareTests.cs --- .../RequestDecompressionMiddlewareTests.cs | 119 ++++++++++++++---- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs index 3e59c39650..388f472308 100644 --- a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -1,44 +1,93 @@ +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.RequestDecompression; +using Xunit; namespace Sentry.AspNetCore.Tests.RequestDecompressionMiddleware; public class RequestDecompressionMiddlewareTests { - [Fact] - public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() + private class Fixture : IDisposable { - // Arrange - var builder = new WebHostBuilder() - .ConfigureServices(services => - { - services.AddRouting(); - services.AddRequestDecompression(); // No options needed for default gzip support - }) - .UseSentry(o => - { - o.Dsn = ValidDsn; - o.MaxRequestBodySize = RequestSize.Always; - }) - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(endpoints => + private TestServer _server; + private HttpClient _client; + private IRequestDecompressionProvider provider; + + private IWebHostBuilder Builder => new WebHostBuilder() + .ConfigureServices(services => { - endpoints.MapPost("/echo", async context => + services.AddRouting(); + if (provider is not null) { - using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); - var body = await reader.ReadToEndAsync(); - await context.Response.WriteAsync(body); + services.AddSingleton(provider); + } + else + { + services.AddRequestDecompression(); + } + }) + .UseSentry(o => + { + o.Dsn = ValidDsn; + o.MaxRequestBodySize = RequestSize.Always; + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapPost("/echo", async context => + { + using var reader = new StreamReader(context.Request.Body, Encoding.UTF8); + var body = await reader.ReadToEndAsync(); + await context.Response.WriteAsync(body); + }); }); }); - }); - using var server = new TestServer(builder); - using var client = server.CreateClient(); + public void FakeDecompressionError() + { + provider = new FlakyDecompressionProvider(); + } + + class FlakyDecompressionProvider : IRequestDecompressionProvider + { + public Stream GetDecompressionStream(HttpContext context) + { + // Simulate a decompression error + throw new InvalidDataException("Flaky decompression error"); + } + } + + public HttpClient GetSut() + { + _server = new TestServer(Builder); + _client = _server.CreateClient(); + return _client; + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } + + private readonly Fixture _fixture = new Fixture(); + + [Fact] + public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() + { + var client = _fixture.GetSut(); var json = "{\"Foo\":\"Bar\"}"; var gzipped = CompressGzip(json); @@ -46,11 +95,24 @@ public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() content.Headers.Add("Content-Encoding", "gzip"); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - // Act var response = await client.PostAsync("/echo", content); var responseBody = await response.Content.ReadAsStringAsync(); - // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(json, responseBody); + } + + [Fact] + public async Task AddRequestDecompression_PlainBodyContent_IsUnaltered() + { + var client = _fixture.GetSut(); + + var json = "{\"Foo\":\"Bar\"}"; + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync("/echo", content); + var responseBody = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(json, responseBody); } @@ -65,4 +127,7 @@ private static byte[] CompressGzip(string str) } return output.ToArray(); } + + // Dummy DSN for Sentry SDK initialization in tests + private const string ValidDsn = "https://public@sentry.local/1"; } From 073c1713b0db185e66293ffbb4dbe452162feed8 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 4 Jul 2025 15:44:53 +1200 Subject: [PATCH 09/16] Decompression error test --- .../FakeSentryServer.cs | 2 +- .../RequestDecompressionMiddlewareTests.cs | 96 +++++++++++++++---- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/test/Sentry.AspNetCore.TestUtils/FakeSentryServer.cs b/test/Sentry.AspNetCore.TestUtils/FakeSentryServer.cs index 495ec0659c..ce52a82f53 100644 --- a/test/Sentry.AspNetCore.TestUtils/FakeSentryServer.cs +++ b/test/Sentry.AspNetCore.TestUtils/FakeSentryServer.cs @@ -5,7 +5,7 @@ namespace Sentry.AspNetCore.TestUtils; -internal static class FakeSentryServer +public static class FakeSentryServer { public static TestServer CreateServer(IReadOnlyCollection handlers) { diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs index 388f472308..773c0853b2 100644 --- a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -1,16 +1,10 @@ -using System.IO.Compression; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RequestDecompression; -using Xunit; +using Sentry.AspNetCore.TestUtils; namespace Sentry.AspNetCore.Tests.RequestDecompressionMiddleware; @@ -20,15 +14,18 @@ private class Fixture : IDisposable { private TestServer _server; private HttpClient _client; - private IRequestDecompressionProvider provider; + private IRequestDecompressionProvider _provider; + public Action ConfigureOptions; - private IWebHostBuilder Builder => new WebHostBuilder() + private IWebHostBuilder GetBuilder() + { + return new WebHostBuilder() .ConfigureServices(services => { services.AddRouting(); - if (provider is not null) + if (_provider is not null) { - services.AddSingleton(provider); + services.AddSingleton(_provider); } else { @@ -39,6 +36,10 @@ private class Fixture : IDisposable { o.Dsn = ValidDsn; o.MaxRequestBodySize = RequestSize.Always; + if (ConfigureOptions is not null) + { + ConfigureOptions(o); + } }) .Configure(app => { @@ -53,13 +54,14 @@ private class Fixture : IDisposable }); }); }); + } public void FakeDecompressionError() { - provider = new FlakyDecompressionProvider(); + _provider = new FlakyDecompressionProvider(); } - class FlakyDecompressionProvider : IRequestDecompressionProvider + private class FlakyDecompressionProvider : IRequestDecompressionProvider { public Stream GetDecompressionStream(HttpContext context) { @@ -70,7 +72,7 @@ public Stream GetDecompressionStream(HttpContext context) public HttpClient GetSut() { - _server = new TestServer(Builder); + _server = new TestServer(GetBuilder()); _client = _server.CreateClient(); return _client; } @@ -82,7 +84,22 @@ public void Dispose() } } - private readonly Fixture _fixture = new Fixture(); + private readonly Fixture _fixture = new (); + + [Fact] + public async Task AddRequestDecompression_PlainBodyContent_IsUnaltered() + { + var client = _fixture.GetSut(); + + var json = "{\"Foo\":\"Bar\"}"; + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync("/echo", content); + var responseBody = await response.Content.ReadAsStringAsync(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(json, responseBody); + } [Fact] public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() @@ -103,18 +120,55 @@ public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() } [Fact] - public async Task AddRequestDecompression_PlainBodyContent_IsUnaltered() + public async Task DecompressionError_SentryCapturesException() { + // Arrange + SentryEvent exceptionEvent = null; + var exceptionProcessor = Substitute.For(); + exceptionProcessor.Process(Arg.Any(), Arg.Do( + evt => exceptionEvent = evt + )); + + var sentry = FakeSentryServer.CreateServer(); + var sentryHttpClient = sentry.CreateClient(); + _fixture.ConfigureOptions = options => + { + options.SentryHttpClientFactory = new DelegateHttpClientFactory(_ => sentryHttpClient); + options.AddExceptionProcessor(exceptionProcessor); + }; + _fixture.FakeDecompressionError(); var client = _fixture.GetSut(); var json = "{\"Foo\":\"Bar\"}"; - var content = new StringContent(json, Encoding.UTF8, "application/json"); + var gzipped = CompressGzip(json); + var content = new ByteArrayContent(gzipped); + content.Headers.Add("Content-Encoding", "gzip"); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - var response = await client.PostAsync("/echo", content); - var responseBody = await response.Content.ReadAsStringAsync(); + // Act + try + { + var _ = await client.PostAsync("/echo", content); + } + catch + { + // We're expecting an exception here... what we're interested in is what happens on the server + } - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(json, responseBody); + // Assert + exceptionEvent.Should().NotBeNull(); + using (new AssertionScope()) + { + exceptionEvent.Tags.Should().Contain(kvp => + kvp.Key == "RequestPath" && + kvp.Value == "/echo" + ); + exceptionEvent.Exception.Should().NotBeNull(); + if (exceptionEvent.Exception is not null) + { + exceptionEvent.Exception.Message.Should().Be("Flaky decompression error"); + } + } } private static byte[] CompressGzip(string str) From 19b5b33a85752ddb74cd7db1466654dc62e4bd01 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 4 Jul 2025 17:35:15 +1200 Subject: [PATCH 10/16] Update RequestDecompressionMiddlewareTests.cs --- .../RequestDecompressionMiddlewareTests.cs | 44 +++++-------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs index 773c0853b2..b54e7e5153 100644 --- a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -15,10 +15,16 @@ private class Fixture : IDisposable private TestServer _server; private HttpClient _client; private IRequestDecompressionProvider _provider; - public Action ConfigureOptions; + public Exception LastException { get; private set; } private IWebHostBuilder GetBuilder() { + var exceptionProcessor = Substitute.For(); + exceptionProcessor.Process(Arg.Do(e => LastException = e), + Arg.Any()); + + var sentry = FakeSentryServer.CreateServer(); + var sentryHttpClient = sentry.CreateClient(); return new WebHostBuilder() .ConfigureServices(services => { @@ -36,10 +42,8 @@ private IWebHostBuilder GetBuilder() { o.Dsn = ValidDsn; o.MaxRequestBodySize = RequestSize.Always; - if (ConfigureOptions is not null) - { - ConfigureOptions(o); - } + o.SentryHttpClientFactory = new DelegateHttpClientFactory(_ => sentryHttpClient); + o.AddExceptionProcessor(exceptionProcessor); }) .Configure(app => { @@ -123,19 +127,6 @@ public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() public async Task DecompressionError_SentryCapturesException() { // Arrange - SentryEvent exceptionEvent = null; - var exceptionProcessor = Substitute.For(); - exceptionProcessor.Process(Arg.Any(), Arg.Do( - evt => exceptionEvent = evt - )); - - var sentry = FakeSentryServer.CreateServer(); - var sentryHttpClient = sentry.CreateClient(); - _fixture.ConfigureOptions = options => - { - options.SentryHttpClientFactory = new DelegateHttpClientFactory(_ => sentryHttpClient); - options.AddExceptionProcessor(exceptionProcessor); - }; _fixture.FakeDecompressionError(); var client = _fixture.GetSut(); @@ -148,7 +139,7 @@ public async Task DecompressionError_SentryCapturesException() // Act try { - var _ = await client.PostAsync("/echo", content); + _ = await client.PostAsync("/echo", content); } catch { @@ -156,18 +147,10 @@ public async Task DecompressionError_SentryCapturesException() } // Assert - exceptionEvent.Should().NotBeNull(); using (new AssertionScope()) { - exceptionEvent.Tags.Should().Contain(kvp => - kvp.Key == "RequestPath" && - kvp.Value == "/echo" - ); - exceptionEvent.Exception.Should().NotBeNull(); - if (exceptionEvent.Exception is not null) - { - exceptionEvent.Exception.Message.Should().Be("Flaky decompression error"); - } + _fixture.LastException.Should().NotBeNull(); + _fixture.LastException?.Message.Should().Be("Flaky decompression error"); } } @@ -181,7 +164,4 @@ private static byte[] CompressGzip(string str) } return output.ToArray(); } - - // Dummy DSN for Sentry SDK initialization in tests - private const string ValidDsn = "https://public@sentry.local/1"; } From 48527479c9f8a9202a451cac25c0530754ab19bb Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 4 Jul 2025 05:56:33 +0000 Subject: [PATCH 11/16] Format code --- .../RequestDecompressionMiddlewareTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs index b54e7e5153..235ef92cd5 100644 --- a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -1,9 +1,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.RequestDecompression; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; using Sentry.AspNetCore.TestUtils; namespace Sentry.AspNetCore.Tests.RequestDecompressionMiddleware; @@ -88,7 +88,7 @@ public void Dispose() } } - private readonly Fixture _fixture = new (); + private readonly Fixture _fixture = new(); [Fact] public async Task AddRequestDecompression_PlainBodyContent_IsUnaltered() From f0afcb577585761d5ce62560b29ffe4a40349d8d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 8 Jul 2025 14:45:14 +1200 Subject: [PATCH 12/16] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12596ec098..c9cc0c0d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ ### Fixes -- When Sentry is configured to capture Request bodies in ASP.NET Core, the uncompressed content is not captured when RequestDecompression middleware is enabled. Previously the request bodies were being captured prior to decompression ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) +- Sentry now correctly decompresses Request bodies in ASP.NET Core when RequestDecompression middleware is enabled. Previously the compressed request bodies were being captured ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) - Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314)) ### Dependencies From 1ce0a283036554ebf091492fd258625e1002140c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 8 Jul 2025 14:46:08 +1200 Subject: [PATCH 13/16] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4428839a..a69575f6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ ### Fixes -- Sentry now correctly decompresses Request bodies in ASP.NET Core when RequestDecompression middleware is enabled. Previously the compressed request bodies were being captured ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) +- Sentry now decompresses Request bodies in ASP.NET Core when RequestDecompression middleware is enabled ([#4315](https://github.com/getsentry/sentry-dotnet/pull/4315)) - Custom ISentryEventProcessors are now run for native iOS events ([#4318](https://github.com/getsentry/sentry-dotnet/pull/4318)) - Crontab validation when capturing checkins ([#4314](https://github.com/getsentry/sentry-dotnet/pull/4314)) From 76ebf4ffb9d16824c56e2222410077e4d1bef284 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 8 Jul 2025 14:53:12 +1200 Subject: [PATCH 14/16] Review feedback --- .../RequestDecompressionMiddleware.cs | 2 +- .../SentryAspNetCoreOptions.cs | 6 ++--- .../BaseRequestPayloadExtractor.cs | 22 +++++++++---------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs b/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs index 848397f13d..ad6576b3f2 100644 --- a/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs +++ b/src/Sentry.AspNetCore/RequestDecompression/RequestDecompressionMiddleware.cs @@ -100,7 +100,7 @@ private void HandleException(Exception e) const string description = "An exception was captured and then re-thrown, when attempting to decompress the request body." + "The web server likely returned a 5xx error code as a result of this exception."; - e.SetSentryMechanism("RequestDecompressionMiddleware", description, handled: false); + e.SetSentryMechanism(nameof(RequestDecompressionMiddleware), description, handled: false); _hub.CaptureException(e); ExceptionDispatchInfo.Capture(e).Throw(); } diff --git a/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs b/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs index e3f6494b4c..345a92d69e 100644 --- a/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs +++ b/src/Sentry.AspNetCore/SentryAspNetCoreOptions.cs @@ -122,15 +122,15 @@ internal void SetEnvironment(IWebHostEnvironment hostingEnvironment) if (hostingEnvironment.IsProduction()) { - Environment = Sentry.Internal.Constants.ProductionEnvironmentSetting; + Environment = Internal.Constants.ProductionEnvironmentSetting; } else if (hostingEnvironment.IsStaging()) { - Environment = Sentry.Internal.Constants.StagingEnvironmentSetting; + Environment = Internal.Constants.StagingEnvironmentSetting; } else if (hostingEnvironment.IsDevelopment()) { - Environment = Sentry.Internal.Constants.DevelopmentEnvironmentSetting; + Environment = Internal.Constants.DevelopmentEnvironmentSetting; } else { diff --git a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs index 7efedc3f44..8517936d72 100644 --- a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs +++ b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs @@ -23,24 +23,22 @@ public abstract class BaseRequestPayloadExtractor : IRequestPayloadExtractor return null; } - // When RequestDecompression is enabled, the RequestDecompressionMiddleware will store a SizeLimitedStream - // in the request body after decompression. Seek operations throw an exception, but we can still read the stream - var originalPosition = request.Body.CanSeek ? request.Body.Position : 0; - try + if (!request.Body.CanSeek) { - if (request.Body.CanSeek) - { - request.Body.Position = 0; - } + // When RequestDecompression is enabled, the RequestDecompressionMiddleware will store a SizeLimitedStream + // in the request body after decompression. Seek operations throw an exception, but we can still read the stream + return DoExtractPayLoad(request); + } + var originalPosition = request.Body.Position; + try + { + request.Body.Position = 0; return DoExtractPayLoad(request); } finally { - if (request.Body.CanSeek) - { - request.Body.Position = originalPosition; - } + request.Body.Position = originalPosition; } } From f2a411bc6681cafe33eaa8be47bf2499eea8aed2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 8 Jul 2025 15:00:23 +1200 Subject: [PATCH 15/16] Update RequestDecompressionMiddlewareTests.cs --- .../RequestDecompressionMiddlewareTests.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs index 235ef92cd5..d3639ca96c 100644 --- a/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/RequestDecompressionMiddleware/RequestDecompressionMiddlewareTests.cs @@ -8,7 +8,7 @@ namespace Sentry.AspNetCore.Tests.RequestDecompressionMiddleware; -public class RequestDecompressionMiddlewareTests +public class RequestDecompressionMiddlewareTests : IDisposable { private class Fixture : IDisposable { @@ -60,7 +60,7 @@ private IWebHostBuilder GetBuilder() }); } - public void FakeDecompressionError() + public void UseFakeDecompressionProvider() { _provider = new FlakyDecompressionProvider(); } @@ -90,6 +90,11 @@ public void Dispose() private readonly Fixture _fixture = new(); + public void Dispose() + { + _fixture.Dispose(); + } + [Fact] public async Task AddRequestDecompression_PlainBodyContent_IsUnaltered() { @@ -127,7 +132,7 @@ public async Task AddRequestDecompression_CompressedBodyContent_IsDecompressed() public async Task DecompressionError_SentryCapturesException() { // Arrange - _fixture.FakeDecompressionError(); + _fixture.UseFakeDecompressionProvider(); var client = _fixture.GetSut(); var json = "{\"Foo\":\"Bar\"}"; From a0b6f78c76b155f46ee139a23546bab8afedfdf6 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 8 Jul 2025 03:28:12 +0000 Subject: [PATCH 16/16] Format code --- src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs index 8517936d72..75ac864886 100644 --- a/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs +++ b/src/Sentry/Extensibility/BaseRequestPayloadExtractor.cs @@ -30,7 +30,7 @@ public abstract class BaseRequestPayloadExtractor : IRequestPayloadExtractor return DoExtractPayLoad(request); } - var originalPosition = request.Body.Position; + var originalPosition = request.Body.Position; try { request.Body.Position = 0;