diff --git a/CHANGELOG.md b/CHANGELOG.md index 6da7e17..47e4306 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed +- **request.dispatcher:** Hide pipeline internals in stack frames + ### Removed [1.0.0]: https://code.geekeey.de/geekeey/request/releases/tag/1.0.0 diff --git a/src/request.dispatcher.tests/StackTraceTests.cs b/src/request.dispatcher.tests/StackTraceTests.cs new file mode 100644 index 0000000..636dfb6 --- /dev/null +++ b/src/request.dispatcher.tests/StackTraceTests.cs @@ -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(); + sc.AddRequestDispatcher(builder => builder + .Add(typeof(FailingScalarHandler)) + .Add(typeof(ScalarOpenBehavior<,>))); + + var provider = sc.BuildServiceProvider(); + var dispatcher = provider.GetRequiredService(); + var request = new FailingScalarRequest(); + + // Act + var exception = await Assert.ThrowsAsync(() => + 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(); + sc.AddRequestDispatcher(builder => builder + .Add(typeof(FailingStreamHandler)) + .Add(typeof(StreamOpenBehavior<,>))); + + var provider = sc.BuildServiceProvider(); + var dispatcher = provider.GetRequiredService(); + var request = new FailingStreamRequest(); + + // Act + var exception = await Assert.ThrowsAsync(() => + 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+<>"); + } +} diff --git a/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs b/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs index 3d90be3..d5ded39 100644 --- a/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs +++ b/src/request.dispatcher.tests/_fixtures/StreamOpenBehavior.cs @@ -1,14 +1,19 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System.Runtime.CompilerServices; + namespace Geekeey.Request.Dispatcher.Tests; public class StreamOpenBehavior(StreamTestTracker tracker) : IStreamRequestBehavior where TRequest : IStreamRequest { - public IAsyncEnumerable HandleAsync(TRequest request, StreamHandlerDelegate next, CancellationToken cancellationToken) + public async IAsyncEnumerable HandleAsync(TRequest request, StreamHandlerDelegate next, [EnumeratorCancellation] CancellationToken cancellationToken) { tracker.Executed = true; - return next(request, cancellationToken); + await foreach (var response in next(request, cancellationToken)) + { + yield return response; + } } } diff --git a/src/request.dispatcher/ScalarRequestInvoker.cs b/src/request.dispatcher/ScalarRequestInvoker.cs index feb02bf..63de03e 100644 --- a/src/request.dispatcher/ScalarRequestInvoker.cs +++ b/src/request.dispatcher/ScalarRequestInvoker.cs @@ -1,6 +1,8 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System.Diagnostics; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -36,7 +38,7 @@ internal sealed class ScalarRequestInvoker : ScalarRequestI static ScalarHandlerDelegate Chain(ScalarHandlerDelegate next, IScalarRequestBehavior filter) { - return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); + return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct); } Task Head(IScalarRequest r, CancellationToken ct) diff --git a/src/request.dispatcher/StreamRequestInvoker.cs b/src/request.dispatcher/StreamRequestInvoker.cs index 6557146..882d538 100644 --- a/src/request.dispatcher/StreamRequestInvoker.cs +++ b/src/request.dispatcher/StreamRequestInvoker.cs @@ -1,6 +1,7 @@ // Copyright (c) The Geekeey Authors // SPDX-License-Identifier: EUPL-1.2 +using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; @@ -41,7 +42,7 @@ internal sealed class StreamRequestInvoker : StreamRequestI static StreamHandlerDelegate Chain(StreamHandlerDelegate next, IStreamRequestBehavior filter) { - return (req, ct) => filter.HandleAsync((TRequest)req, next, ct); + return [StackTraceHidden] (req, ct) => filter.HandleAsync((TRequest)req, next, ct); } IAsyncEnumerable Head(IStreamRequest r, CancellationToken ct)