Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions build/common.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<PackageReleaseNotes>https://github.com/exceptionless/Exceptionless.Net/releases</PackageReleaseNotes>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
<MinVerTagPrefix>v</MinVerTagPrefix>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
Comment thread
niemyjski marked this conversation as resolved.

<Copyright>Copyright (c) 2025 Exceptionless. All rights reserved.</Copyright>
<Authors>Exceptionless</Authors>
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": "9.0.100",
Comment thread
niemyjski marked this conversation as resolved.
Outdated
"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
18 changes: 10 additions & 8 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,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);
Expand Down Expand Up @@ -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);
}
Comment thread
niemyjski marked this conversation as resolved.
Comment thread
niemyjski marked this conversation as resolved.

// 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
24 changes: 13 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,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);
}
Comment thread
niemyjski marked this conversation as resolved.

int maxDataToRead = contentLength == 0 ? MAX_BODY_SIZE : contentLength;
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

// 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 +235,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
7 changes: 5 additions & 2 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);

// 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.
Comment thread
niemyjski marked this conversation as resolved.
Outdated
//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;
Expand Down
4 changes: 3 additions & 1 deletion test/Exceptionless.Tests/Exceptionless.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>
47 changes: 47 additions & 0 deletions test/Exceptionless.Tests/Platforms/AspNetCoreRequestInfoTests.cs
Original file line number Diff line number Diff line change
@@ -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");
Comment thread
niemyjski marked this conversation as resolved.
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<string>(requestInfo.PostData));
Assert.Equal(5L, context.Request.Body.Position);
}
Comment thread
niemyjski marked this conversation as resolved.

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;
}
}
}
Loading