Compare commits

...

1 commit

Author SHA1 Message Date
5c2911ea6c
feat: hide pipelines internals from stack trace
Some checks failed
default / dotnet-default-workflow (pull_request) Failing after 3m2s
2026-05-29 22:53:58 +02:00
5 changed files with 85 additions and 4 deletions

View file

@ -32,6 +32,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Changed ### Changed
- **request.dispatcher:** Hide pipeline internals in stack frames
### Removed ### Removed
[1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0 [1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0

View file

@ -0,0 +1,73 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using Microsoft.Extensions.DependencyInjection;
namespace Geekeey.Request.Dispatcher.Tests;
internal sealed class StackTraceTests
{
[Test]
public async Task I_can_not_see_pipeline_internals_are_hidden_from_stack_trace_scalar()
{
// Arrange
var sc = new ServiceCollection();
sc.AddSingleton<ScalarTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingScalarHandler))
.Add(typeof(ScalarOpenBehavior<,>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new FailingScalarRequest();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
dispatcher.DispatchAsync(request));
// Assert
await Assert.That(exception).IsNotNull();
var stackTrace = await Assert.That(exception.StackTrace).IsNotNull();
await Assert.That(stackTrace).Contains(nameof(FailingScalarHandler));
await Assert.That(stackTrace).Contains(nameof(ScalarOpenBehavior<,>));
// 3. Verify that the internal lambda from ScalarRequestInvoker.Chain is HIDDEN.
// In C#, these lambdas usually appear as "ScalarRequestInvoker`2.<>c__DisplayClass..." or similar.
// Since we added [StackTraceHidden], this frame should be omitted.
await Assert.That(stackTrace).DoesNotContain("ScalarRequestInvoker+<>");
}
[Test]
public async Task I_can_not_see_pipeline_internals_are_hidden_from_stack_trace_stream()
{
// Arrange
var sc = new ServiceCollection();
sc.AddSingleton<StreamTestTracker>();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(FailingStreamHandler))
.Add(typeof(StreamOpenBehavior<,>)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new FailingStreamRequest();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
dispatcher.DispatchAsync(request).ToListAsync().AsTask());
// Assert
await Assert.That(exception).IsNotNull();
var stackTrace = await Assert.That(exception.StackTrace).IsNotNull();
await Assert.That(stackTrace).Contains(nameof(FailingStreamHandler));
await Assert.That(stackTrace).Contains(nameof(StreamOpenBehavior<,>));
// 3. Verify that the internal lambda from ScalarRequestInvoker.Chain is HIDDEN.
// In C#, these lambdas usually appear as "ScalarRequestInvoker`2.<>c__DisplayClass..." or similar.
// Since we added [StackTraceHidden], this frame should be omitted.
await Assert.That(stackTrace).DoesNotContain("StreamRequestInvoker+<>");
}
}

View file

@ -6,9 +6,12 @@ namespace Geekeey.Request.Dispatcher.Tests;
public class StreamOpenBehavior<TRequest, TResponse>(StreamTestTracker tracker) : IStreamRequestBehavior<TRequest, TResponse> public class StreamOpenBehavior<TRequest, TResponse>(StreamTestTracker tracker) : IStreamRequestBehavior<TRequest, TResponse>
where TRequest : IStreamRequest<TResponse> where TRequest : IStreamRequest<TResponse>
{ {
public IAsyncEnumerable<TResponse> HandleAsync(TRequest request, StreamHandlerDelegate<TResponse> next, CancellationToken cancellationToken) public async IAsyncEnumerable<TResponse> HandleAsync(TRequest request, StreamHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{ {
tracker.Executed = true; tracker.Executed = true;
return next(request, cancellationToken); await foreach (var response in next(request, cancellationToken))
{
yield return response;
}
} }
} }

View file

@ -1,6 +1,8 @@
// Copyright (c) The Geekeey Authors // Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -36,7 +38,7 @@ internal sealed class ScalarRequestInvoker<TRequest, TResponse> : ScalarRequestI
static ScalarHandlerDelegate<TResponse> Chain(ScalarHandlerDelegate<TResponse> next, IScalarRequestBehavior<TRequest, TResponse> filter) static ScalarHandlerDelegate<TResponse> Chain(ScalarHandlerDelegate<TResponse> next, IScalarRequestBehavior<TRequest, TResponse> filter)
{ {
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
} }
Task<TResponse> Head(IScalarRequest<TResponse> r, CancellationToken ct) Task<TResponse> Head(IScalarRequest<TResponse> r, CancellationToken ct)

View file

@ -1,6 +1,7 @@
// Copyright (c) The Geekeey Authors // Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2 // SPDX-License-Identifier: EUPL-1.2
using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -41,7 +42,7 @@ internal sealed class StreamRequestInvoker<TRequest, TResponse> : StreamRequestI
static StreamHandlerDelegate<TResponse> Chain(StreamHandlerDelegate<TResponse> next, IStreamRequestBehavior<TRequest, TResponse> filter) static StreamHandlerDelegate<TResponse> Chain(StreamHandlerDelegate<TResponse> next, IStreamRequestBehavior<TRequest, TResponse> filter)
{ {
return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct);
} }
IAsyncEnumerable<TResponse> Head(IStreamRequest<TResponse> r, CancellationToken ct) IAsyncEnumerable<TResponse> Head(IStreamRequest<TResponse> r, CancellationToken ct)