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..76e6fddb 100644
--- a/build/common.props
+++ b/build/common.props
@@ -6,8 +6,9 @@
https://github.com/exceptionless/Exceptionless.Net/releases
true
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/global.json b/global.json
index 989a69ca..1e7fdfa9 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "10.0.100",
"rollForward": "latestMinor"
}
-}
\ No newline at end of file
+}
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..146ec89a 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,13 +61,8 @@ 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();
- if(contentLength == 0) {
+ long contentLength = context.Request.ContentLength.GetValueOrDefault();
+ if (contentLength == 0) {
string message = "Content-length was zero, empty post.";
log.Debug(message);
return message;
@@ -98,6 +95,14 @@ 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);
+ }
+
// 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..dd864dd8 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,15 @@ private static object GetPostData(HttpContextBase context, ExceptionlessConfigur
return message;
}
- var maxDataToRead = contentLength == 0 ? MAX_BODY_SIZE : contentLength;
+ // 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);
+ }
+
+ 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 +237,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..b3f5b521 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);
- // TODO: support getting post data asyncly.
- //if (config.IncludePostData && context.Request.Method != HttpMethod.Get)
+ // 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 asynchronously.
+ //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..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
@@ -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..599c87db
--- /dev/null
+++ b/test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs
@@ -0,0 +1,84 @@
+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);
+ }
+
+ [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();
+ context.Request.Method = HttpMethods.Post;
+ context.Request.ContentType = "application/json";
+ context.Request.Body = new MemoryStream(bodyBytes);
+ context.Request.ContentLength = bodyBytes.Length;
+ return context;
+ }
+
+ 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"
+ });
+ return context;
+ }
+ }
+}