Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/build-osx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/build-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
<PackageReleaseNotes>https://github.com/exceptionless/Exceptionless.Net/releases</PackageReleaseNotes>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
<MinVerTagPrefix>v</MinVerTagPrefix>
<EnableWindowsTargeting>true</EnableWindowsTargeting>

<Copyright>Copyright (c) 2025 Exceptionless. All rights reserved.</Copyright>
<Copyright>Copyright © $([System.DateTime]::Now.ToString(yyyy)) Exceptionless. All rights reserved.</Copyright>
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MSBuild property function call uses Now.ToString(yyyy) without quoting the format string. MSBuild property functions typically require string arguments to be quoted (e.g., ToString('yyyy')), otherwise the argument may be treated as an empty/unresolved expression and not yield the intended year.

Suggested change
<Copyright>Copyright © $([System.DateTime]::Now.ToString(yyyy)) Exceptionless. All rights reserved.</Copyright>
<Copyright>Copyright © $([System.DateTime]::Now.ToString('yyyy')) Exceptionless. All rights reserved.</Copyright>

Copilot uses AI. Check for mistakes.
<Authors>Exceptionless</Authors>
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
Expand Down
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.100",
"rollForward": "latestMinor"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ public Dictionary<string, string> 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.
Expand All @@ -44,4 +46,4 @@ public Dictionary<string, string> Get() {
throw new Exception($"Unhandled Exception: {Guid.NewGuid()}");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ public static IApplicationBuilder UseExceptionless(this IApplicationBuilder app,
/// </summary>
/// <param name="context">The http context to gather information from.</param>
/// <param name="config">The config.</param>
public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config) {
return RequestInfoCollector.Collect(context, config);
/// <param name="isUnhandledError">Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream.</param>
public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) {
return RequestInfoCollector.Collect(context, config, isUnhandledError);
}

/// <summary>
Expand Down
23 changes: 14 additions & 9 deletions src/Platforms/Exceptionless.AspNetCore/RequestInfoCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 6 additions & 4 deletions src/Platforms/Exceptionless.Web/ExceptionlessWebExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,25 @@ public static class ExceptionlessWebExtensions {
/// </summary>
/// <param name="context">The http context to gather information from.</param>
/// <param name="config">The config.</param>
public static RequestInfo GetRequestInfo(this HttpContext context, ExceptionlessConfiguration config) {
/// <param name="isUnhandledError">Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream.</param>
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);
}

/// <summary>
/// Adds the current request info.
/// </summary>
/// <param name="context">The http context to gather information from.</param>
/// <param name="config">The config.</param>
public static RequestInfo GetRequestInfo(this HttpContextBase context, ExceptionlessConfiguration config) {
/// <param name="isUnhandledError">Whether this is an unhandled error. POST data is only collected for unhandled errors to avoid consuming the request stream.</param>
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);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Platforms/Exceptionless.Web/ExceptionlessWebPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
26 changes: 15 additions & 11 deletions src/Platforms/Exceptionless.Web/RequestInfoCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -233,4 +237,4 @@ private static string GetUserIpAddress(HttpContextBase context) {
return clientIp;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ private static void ReplaceHttpErrorHandler(HttpConfiguration config, Exceptionl
/// </summary>
/// <param name="context">The http action context to gather information from.</param>
/// <param name="config">The config.</param>
public static RequestInfo GetRequestInfo(this HttpActionContext context, ExceptionlessConfiguration config) {
return RequestInfoCollector.Collect(context, config);
/// <param name="isUnhandledError">Whether this is an unhandled error. POST data collection is not implemented for WebApi.</param>
public static RequestInfo GetRequestInfo(this HttpActionContext context, ExceptionlessConfiguration config, bool isUnhandledError = false) {
return RequestInfoCollector.Collect(context, config, isUnhandledError);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
9 changes: 6 additions & 3 deletions src/Platforms/Exceptionless.WebApi/RequestInfoCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
10 changes: 6 additions & 4 deletions test/Exceptionless.Tests/Exceptionless.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<Import Project="..\..\build\common.props" />

<PropertyGroup Condition=" '$(OS)' != 'Windows_NT' ">
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(OS)' == 'Windows_NT' ">
<TargetFrameworks>net8.0;net462</TargetFrameworks>
<TargetFrameworks>net10.0;net462</TargetFrameworks>
</PropertyGroup>

<PropertyGroup>
Expand All @@ -19,7 +19,7 @@
<None Include="app.config" />
</ItemGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0' " Label="Build">
<PropertyGroup Condition=" '$(TargetFramework)' == 'net10.0' " Label="Build">
<DefineConstants>$(DefineConstants);NETSTANDARD;NETSTANDARD2_0</DefineConstants>
</PropertyGroup>

Expand All @@ -29,10 +29,12 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Exceptionless\Exceptionless.csproj" />
<ProjectReference Include="..\..\src\Platforms\Exceptionless.AspNetCore\Exceptionless.AspNetCore.csproj" />
<ProjectReference Include="..\Exceptionless.TestHarness\Exceptionless.TestHarness.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
<PackageReference Include="Moq" Version="4.20.72" />
Expand All @@ -54,4 +56,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
</Project>
Loading
Loading