From 468ba0fe919cb41ad91239b5b7c14cce5419bd68 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 24 Mar 2026 23:14:08 -0500 Subject: [PATCH 1/5] chore(ci): upgrade workflow actions and sdk setup --- .github/workflows/build-linux.yml | 9 +++++---- .github/workflows/build-osx.yml | 9 +++++---- .github/workflows/build-windows.yml | 9 +++++---- build/common.props | 1 + global.json | 4 ++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index c6a99cf2..dd6677c7 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -2,18 +2,19 @@ name: Build Linux on: [push, pull_request] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 jobs: build: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Setup .NET Core 8 - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + global-json-file: global.json - name: Build Reason run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" - name: Build Version diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index d7f08e32..83852de2 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -2,18 +2,19 @@ name: Build OSX on: [push, pull_request] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 jobs: build: runs-on: macOS-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Setup .NET Core 8 - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + global-json-file: global.json - name: Build Reason run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" - name: Build Version diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index dc6af569..3efed70e 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -2,18 +2,19 @@ name: Build Windows on: [push, pull_request] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 jobs: build: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - - name: Setup .NET Core 8 - uses: actions/setup-dotnet@v4 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 with: - dotnet-version: 8.x + global-json-file: global.json - name: Build Reason run: "echo ref: ${{github.ref}} event: ${{github.event_name}}" - name: Build Version diff --git a/build/common.props b/build/common.props index b2b0ca80..abf62996 100644 --- a/build/common.props +++ b/build/common.props @@ -6,6 +6,7 @@ https://github.com/exceptionless/Exceptionless.Net/releases true v + true Copyright (c) 2025 Exceptionless. All rights reserved. Exceptionless diff --git a/global.json b/global.json index 989a69ca..2bc13e80 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "rollForward": "latestMinor" } -} \ No newline at end of file +} From c1193d9276c72f95f45cbfdebb8467f8912fe682 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 24 Mar 2026 23:14:17 -0500 Subject: [PATCH 2/5] fix(request-info): avoid reading handled post data --- .../Controllers/ValuesController.cs | 8 ++-- .../ExceptionlessAspNetCorePlugin.cs | 2 +- .../ExceptionlessExtensions.cs | 5 +- .../RequestInfoCollector.cs | 18 +++---- .../ExceptionlessWebExtensions.cs | 10 ++-- .../ExceptionlessWebPlugin.cs | 2 +- .../Exceptionless.Web/RequestInfoCollector.cs | 24 +++++----- .../ExceptionlessWebApiExtensions.cs | 5 +- .../ExceptionlessWebApiPlugin.cs | 2 +- .../RequestInfoCollector.cs | 7 ++- .../Exceptionless.Tests.csproj | 4 +- .../Platforms/AspNetCoreRequestInfoTests.cs | 47 +++++++++++++++++++ 12 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs diff --git a/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs b/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs index b6bce7b5..964450dc 100644 --- a/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs +++ b/samples/Exceptionless.SampleAspNetCore/Controllers/ValuesController.cs @@ -26,14 +26,16 @@ public Dictionary Get() { try { throw new Exception($"Handled Exception: {Guid.NewGuid()}"); - } catch (Exception handledException) { + } + catch (Exception handledException) { // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the client instance from DI. handledException.ToExceptionless(_exceptionlessClient).Submit(); } try { throw new Exception($"Handled Exception (Default Client): {Guid.NewGuid()}"); - } catch (Exception handledException) { + } + catch (Exception handledException) { // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the default client instance (ExceptionlessClient.Default). // This works and is convenient, but its generally not recommended to use static singleton instances because it makes testing and // other things harder. @@ -44,4 +46,4 @@ public Dictionary Get() { throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessAspNetCorePlugin.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessAspNetCorePlugin.cs index f6fd4b52..b7def64b 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessAspNetCorePlugin.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessAspNetCorePlugin.cs @@ -32,7 +32,7 @@ public void Run(EventPluginContext context) { return; try { - ri = httpContext.GetRequestInfo(context.Client.Configuration); + ri = httpContext.GetRequestInfo(context.Client.Configuration, context.ContextData.IsUnhandledError); } catch (Exception ex) { context.Log.Error(typeof(ExceptionlessAspNetCorePlugin), ex, "Error adding request info."); } diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs index 240767d3..cbe6f0c9 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessExtensions.cs @@ -68,8 +68,9 @@ public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app, /// /// The http context to gather information from. /// The config. - public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config) { - return RequestInfoCollector.Collect(context, config); + /// Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream. + public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) { + return RequestInfoCollector.Collect(context, config, isUnhandledError); } /// diff --git a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs index 31bebdd2..760415d9 100644 --- a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs @@ -16,7 +16,7 @@ public static class RequestInfoCollector { private const int MAX_BODY_SIZE = 50 * 1024; private const int MAX_DATA_ITEM_LENGTH = 1000; - public static RequestInfo Collect(HttpContext context, ExceptionlessConfiguration config) { + public static RequestInfo Collect(HttpContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) { if (context == null) return null; @@ -50,7 +50,9 @@ public static RequestInfo Collect(HttpContext context, ExceptionlessConfiguratio if (config.IncludeQueryString) info.QueryString = context.Request.Query.ToDictionary(exclusionList); - if (config.IncludePostData && !String.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) + // Only collect POST data for unhandled errors to avoid consuming the request stream + // and breaking model binding for handled errors where the app continues processing. + if (config.IncludePostData && isUnhandledError && !String.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase)) info.PostData = GetPostData(context, config, exclusionList); return info; @@ -59,12 +61,7 @@ public static RequestInfo Collect(HttpContext context, ExceptionlessConfiguratio private static object GetPostData(HttpContext context, ExceptionlessConfiguration config, string[] exclusionList) { var log = config.Resolver.GetLog(); - if (context.Request.HasFormContentType && context.Request.Form.Count > 0) { - log.Debug("Reading POST data from Request.Form"); - return context.Request.Form.ToDictionary(exclusionList); - } - - var contentLength = context.Request.ContentLength.GetValueOrDefault(); + long contentLength = context.Request.ContentLength.GetValueOrDefault(); if(contentLength == 0) { string message = "Content-length was zero, empty post."; log.Debug(message); @@ -98,6 +95,11 @@ private static object GetPostData(HttpContext context, ExceptionlessConfiguratio return message; } + if (context.Request.HasFormContentType && context.Request.Form.Count > 0) { + log.Debug("Reading POST data from Request.Form"); + return context.Request.Form.ToDictionary(exclusionList); + } + // pass default values, except for leaveOpen: true. This prevents us from disposing the underlying stream using (var inputStream = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true)) { var sb = new StringBuilder(); diff --git a/src/Platforms/Exceptionless.Web/ExceptionlessWebExtensions.cs b/src/Platforms/Exceptionless.Web/ExceptionlessWebExtensions.cs index 6fa97f19..50fecd2f 100644 --- a/src/Platforms/Exceptionless.Web/ExceptionlessWebExtensions.cs +++ b/src/Platforms/Exceptionless.Web/ExceptionlessWebExtensions.cs @@ -11,11 +11,12 @@ public static class ExceptionlessWebExtensions { /// /// The http context to gather information from. /// The config. - public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config) { + /// Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream. + public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) { if (context == null) return null; - return GetRequestInfo(context.ToWrapped(), config); + return GetRequestInfo(context.ToWrapped(), config, isUnhandledError); } /// @@ -23,11 +24,12 @@ public static RequestInfo GetRequestInfo(this HttpContext context, Exceptionless /// /// The http context to gather information from. /// The config. - public static RequestInfo GetRequestInfo(this HttpContextBase context, ExceptionlessConfiguration config) { + /// Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream. + public static RequestInfo GetRequestInfo(this HttpContextBase context, ExceptionlessConfiguration config, bool isUnhandledError = false) { if (context == null && HttpContext.Current != null) context = HttpContext.Current.ToWrapped(); - return RequestInfoCollector.Collect(context, config); + return RequestInfoCollector.Collect(context, config, isUnhandledError); } /// diff --git a/src/Platforms/Exceptionless.Web/ExceptionlessWebPlugin.cs b/src/Platforms/Exceptionless.Web/ExceptionlessWebPlugin.cs index 856732a5..703b0b47 100644 --- a/src/Platforms/Exceptionless.Web/ExceptionlessWebPlugin.cs +++ b/src/Platforms/Exceptionless.Web/ExceptionlessWebPlugin.cs @@ -35,7 +35,7 @@ public void Run(EventPluginContext context) { return; try { - ri = httpContext.GetRequestInfo(context.Client.Configuration); + ri = httpContext.GetRequestInfo(context.Client.Configuration, context.ContextData.IsUnhandledError); } catch (Exception ex) { context.Log.Error(typeof(ExceptionlessWebPlugin), ex, "Error adding request info."); } diff --git a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs index ebebdb64..2a9857a0 100644 --- a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs @@ -15,7 +15,7 @@ internal static class RequestInfoCollector { private const int MAX_BODY_SIZE = 50 * 1024; private const int MAX_DATA_ITEM_LENGTH = 1000; - public static RequestInfo Collect(HttpContextBase context, ExceptionlessConfiguration config) { + public static RequestInfo Collect(HttpContextBase context, ExceptionlessConfiguration config, bool isUnhandledError = false) { if (context == null) return null; @@ -63,7 +63,9 @@ public static RequestInfo Collect(HttpContextBase context, ExceptionlessConfigur } } - if (config.IncludePostData && !String.Equals(context.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + // Only collect POST data for unhandled errors to avoid consuming the request stream + // and breaking model binding for handled errors where the app continues processing. + if (config.IncludePostData && isUnhandledError && !String.Equals(context.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) info.PostData = GetPostData(context, config, exclusionList); return info; @@ -72,13 +74,7 @@ public static RequestInfo Collect(HttpContextBase context, ExceptionlessConfigur private static object GetPostData(HttpContextBase context, ExceptionlessConfiguration config, string[] exclusionList) { var log = config.Resolver.GetLog(); - if (context.Request.Form.Count > 0) { - log.Debug("Reading POST data from Request.Form"); - - return context.Request.Form.ToDictionary(exclusionList); - } - - var contentLength = context.Request.ContentLength; + int contentLength = context.Request.ContentLength; if (contentLength == 0) { string message = "Content-length was zero, empty post."; log.Debug(message); @@ -112,7 +108,13 @@ private static object GetPostData(HttpContextBase context, ExceptionlessConfigur return message; } - var maxDataToRead = contentLength == 0 ? MAX_BODY_SIZE : contentLength; + if (context.Request.Form.Count > 0) { + log.Debug("Reading POST data from Request.Form"); + + return context.Request.Form.ToDictionary(exclusionList); + } + + int maxDataToRead = contentLength == 0 ? MAX_BODY_SIZE : contentLength; // pass default values, except for leaveOpen: true. This prevents us from disposing the underlying stream using (var inputStream = new StreamReader(context.Request.InputStream, Encoding.UTF8, true, 1024, true)) { @@ -233,4 +235,4 @@ private static string GetUserIpAddress(HttpContextBase context) { return clientIp; } } -} \ No newline at end of file +} diff --git a/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiExtensions.cs b/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiExtensions.cs index 338d35eb..b2fa787e 100644 --- a/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiExtensions.cs +++ b/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiExtensions.cs @@ -65,8 +65,9 @@ private static void ReplaceHttpErrorHandler(HttpConfiguration config, Exceptionl /// /// The http action context to gather information from. /// The config. - public static RequestInfo GetRequestInfo(this HttpActionContext context, ExceptionlessConfiguration config) { - return RequestInfoCollector.Collect(context, config); + /// Whether this is an unhandled error. POST data collection is not implemented for WebApi. + public static RequestInfo GetRequestInfo(this HttpActionContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) { + return RequestInfoCollector.Collect(context, config, isUnhandledError); } /// diff --git a/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiPlugin.cs b/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiPlugin.cs index 3328cdd8..0fdba372 100644 --- a/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiPlugin.cs +++ b/src/Platforms/Exceptionless.WebApi/ExceptionlessWebApiPlugin.cs @@ -29,7 +29,7 @@ public void Run(EventPluginContext context) { return; try { - ri = actionContext.GetRequestInfo(context.Client.Configuration); + ri = actionContext.GetRequestInfo(context.Client.Configuration, context.ContextData.IsUnhandledError); } catch (Exception ex) { context.Log.Error(typeof(ExceptionlessWebApiPlugin), ex, "Error adding request info."); } diff --git a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs index 7f5298ee..33fc2be2 100644 --- a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs @@ -14,7 +14,7 @@ namespace Exceptionless.ExtendedData { internal static class RequestInfoCollector { private const int MAX_DATA_ITEM_LENGTH = 1000; - public static RequestInfo Collect(HttpActionContext context, ExceptionlessConfiguration config) { + public static RequestInfo Collect(HttpActionContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) { if (context == null) return null; @@ -48,8 +48,11 @@ public static RequestInfo Collect(HttpActionContext context, ExceptionlessConfig if (config.IncludeQueryString) info.QueryString = context.Request.RequestUri.ParseQueryString().ToDictionary(exclusionList); + // POST data collection is not implemented for WebApi due to async complexities and + // the difficulty of reading the request body without interfering with model binding. + // Other platforms (AspNetCore, Web) now only collect POST data for unhandled errors. // TODO: support getting post data asyncly. - //if (config.IncludePostData && context.Request.Method != HttpMethod.Get) + //if (config.IncludePostData && isUnhandledError && context.Request.Method != HttpMethod.Get) // info.PostData = GetPostData(context, config, exclusionList); return info; diff --git a/test/Exceptionless.Tests/Exceptionless.Tests.csproj b/test/Exceptionless.Tests/Exceptionless.Tests.csproj index 0bfb4e74..81ea618c 100644 --- a/test/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/test/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -29,10 +29,12 @@ + + @@ -54,4 +56,4 @@ PreserveNewest - \ No newline at end of file + diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs new file mode 100644 index 00000000..d709010f --- /dev/null +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Text; +using Exceptionless; +using Exceptionless.Dependency; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Exceptionless.Tests.Platforms { + public class AspNetCoreRequestInfoTests { + [Fact] + public void GetRequestInfo_DoesNotReadPostData_ForHandledErrors() { + var context = CreateHttpContext("hello=world"); + var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); + + var requestInfo = context.GetRequestInfo(config); + + Assert.NotNull(requestInfo); + Assert.Null(requestInfo.PostData); + Assert.Equal(0L, context.Request.Body.Position); + } + + [Fact] + public void GetRequestInfo_ReadsAndRestoresPostData_ForUnhandledErrors() { + const string body = "{\"hello\":\"world\"}"; + var context = CreateHttpContext(body); + var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); + + context.Request.Body.Position = 5; + + var requestInfo = context.GetRequestInfo(config, isUnhandledError: true); + + Assert.NotNull(requestInfo); + Assert.Equal(body, Assert.IsType(requestInfo.PostData)); + Assert.Equal(5L, context.Request.Body.Position); + } + + private static DefaultHttpContext CreateHttpContext(string body) { + var bodyBytes = Encoding.UTF8.GetBytes(body); + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(bodyBytes); + context.Request.ContentLength = bodyBytes.Length; + return context; + } + } +} From 5f1c8405960eeb6b9dad82eca275dd3956595e0b Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 07:55:41 -0500 Subject: [PATCH 3/5] chore: update .NET SDK version to 10.0.100 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 2bc13e80..1e7fdfa9 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "10.0.100", "rollForward": "latestMinor" } } From 539861cd3b435e7a331751802633a5498d1b9d3e Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 08:14:54 -0500 Subject: [PATCH 4/5] fix(request-info): restore stream position and update to .NET 10 Restores the request body stream position after accessing form data in AspNetCore and Web platforms. This commit also updates the test project to target .NET 10 and migrates the copyright notice to use a dynamic year. --- build/common.props | 2 +- .../RequestInfoCollector.cs | 5 ++- .../Exceptionless.Web/RequestInfoCollector.cs | 4 ++- .../RequestInfoCollector.cs | 2 +- .../Exceptionless.Tests.csproj | 6 ++-- .../Platforms/AspNetCoreRequestInfoTests.cs | 33 +++++++++++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/build/common.props b/build/common.props index abf62996..76e6fddb 100644 --- a/build/common.props +++ b/build/common.props @@ -8,7 +8,7 @@ v true - Copyright (c) 2025 Exceptionless. All rights reserved. + Copyright © $([System.DateTime]::Now.ToString(yyyy)) Exceptionless. All rights reserved. Exceptionless $(NoWarn);CS1591;NU1701 true diff --git a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs index 760415d9..146ec89a 100644 --- a/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs @@ -62,7 +62,7 @@ private static object GetPostData(HttpContext context, ExceptionlessConfiguratio var log = config.Resolver.GetLog(); long contentLength = context.Request.ContentLength.GetValueOrDefault(); - if(contentLength == 0) { + if (contentLength == 0) { string message = "Content-length was zero, empty post."; log.Debug(message); return message; @@ -95,8 +95,11 @@ private static object GetPostData(HttpContext context, ExceptionlessConfiguratio return message; } + // Form check must come after seekability and position checks above: accessing + // Request.Form triggers reading the request body stream. if (context.Request.HasFormContentType && context.Request.Form.Count > 0) { log.Debug("Reading POST data from Request.Form"); + context.Request.Body.Position = originalPosition; return context.Request.Form.ToDictionary(exclusionList); } diff --git a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs index 2a9857a0..dd864dd8 100644 --- a/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.Web/RequestInfoCollector.cs @@ -108,9 +108,11 @@ private static object GetPostData(HttpContextBase context, ExceptionlessConfigur return message; } + // Form check must come after seekability and position checks above: accessing + // Request.Form triggers reading the request body stream. if (context.Request.Form.Count > 0) { log.Debug("Reading POST data from Request.Form"); - + context.Request.InputStream.Position = originalPosition; return context.Request.Form.ToDictionary(exclusionList); } diff --git a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs index 33fc2be2..b3f5b521 100644 --- a/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs +++ b/src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs @@ -51,7 +51,7 @@ public static RequestInfo Collect(HttpActionContext context, ExceptionlessConfig // POST data collection is not implemented for WebApi due to async complexities and // the difficulty of reading the request body without interfering with model binding. // Other platforms (AspNetCore, Web) now only collect POST data for unhandled errors. - // TODO: support getting post data asyncly. + // TODO: support getting post data asynchronously. //if (config.IncludePostData && isUnhandledError && context.Request.Method != HttpMethod.Get) // info.PostData = GetPostData(context, config, exclusionList); diff --git a/test/Exceptionless.Tests/Exceptionless.Tests.csproj b/test/Exceptionless.Tests/Exceptionless.Tests.csproj index 81ea618c..f4f37a35 100644 --- a/test/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/test/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -2,10 +2,10 @@ -net8.0 +net10.0 -net8.0;net462 +net10.0;net462 @@ -19,7 +19,7 @@ - + $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs index d709010f..f405b5ed 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs @@ -1,19 +1,24 @@ +using System.Collections.Generic; using System.IO; using System.Text; using Exceptionless; using Exceptionless.Dependency; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; using Xunit; namespace Exceptionless.Tests.Platforms { public class AspNetCoreRequestInfoTests { [Fact] public void GetRequestInfo_DoesNotReadPostData_ForHandledErrors() { + // Arrange var context = CreateHttpContext("hello=world"); var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); + // Act var requestInfo = context.GetRequestInfo(config); + // Assert Assert.NotNull(requestInfo); Assert.Null(requestInfo.PostData); Assert.Equal(0L, context.Request.Body.Position); @@ -21,19 +26,37 @@ public void GetRequestInfo_DoesNotReadPostData_ForHandledErrors() { [Fact] public void GetRequestInfo_ReadsAndRestoresPostData_ForUnhandledErrors() { + // Arrange const string body = "{\"hello\":\"world\"}"; var context = CreateHttpContext(body); var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); context.Request.Body.Position = 5; + // Act var requestInfo = context.GetRequestInfo(config, isUnhandledError: true); + // Assert Assert.NotNull(requestInfo); Assert.Equal(body, Assert.IsType(requestInfo.PostData)); Assert.Equal(5L, context.Request.Body.Position); } + [Fact] + public void GetRequestInfo_ReadsFormData_ForUnhandledErrors() { + // Arrange + var context = CreateFormHttpContext(); + var config = new ExceptionlessConfiguration(DependencyResolver.CreateDefault()); + + // Act + var requestInfo = context.GetRequestInfo(config, isUnhandledError: true); + + // Assert + Assert.NotNull(requestInfo); + var postData = Assert.IsType>(requestInfo.PostData); + Assert.Equal("world", postData["name"]); + } + private static DefaultHttpContext CreateHttpContext(string body) { var bodyBytes = Encoding.UTF8.GetBytes(body); var context = new DefaultHttpContext(); @@ -43,5 +66,15 @@ private static DefaultHttpContext CreateHttpContext(string body) { context.Request.ContentLength = bodyBytes.Length; return context; } + + private static DefaultHttpContext CreateFormHttpContext() { + var context = new DefaultHttpContext(); + context.Request.Method = HttpMethods.Post; + context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.Form = new FormCollection(new Dictionary { + ["name"] = "world" + }); + return context; + } } } From 49a2c0fd0f32db88f7297ec5aa7acd2ea5968626 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Wed, 25 Mar 2026 08:18:27 -0500 Subject: [PATCH 5/5] fix(request-info): initialize request body for form data tests --- .../Platforms/AspNetCoreRequestInfoTests.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs index f405b5ed..599c87db 100644 --- a/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs +++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs @@ -68,9 +68,13 @@ private static DefaultHttpContext CreateHttpContext(string body) { } private static DefaultHttpContext CreateFormHttpContext() { + const string formBody = "name=world"; + var bodyBytes = Encoding.UTF8.GetBytes(formBody); var context = new DefaultHttpContext(); context.Request.Method = HttpMethods.Post; context.Request.ContentType = "application/x-www-form-urlencoded"; + context.Request.ContentLength = bodyBytes.Length; + context.Request.Body = new MemoryStream(bodyBytes); context.Request.Form = new FormCollection(new Dictionary { ["name"] = "world" });