You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Obfuscator async stream methods read the source synchronously, so TryObfuscateAsync throws on ASP.NET Core request bodies, even for Hex/Base64 whose encoders are already async #172
Every stream async member of IObfuscationProvider is still a Task.Run wrapper over the synchronous stream path (IObfuscationProvider.cs#L100-L129, #L204-L233), and no obfuscator declares its own async primitives. So TryObfuscateAsync(Stream, Stream) ends up calling Stream.Read on a pool thread.
HexObfuscationProvider and Base64ObfuscationProvider show this most clearly. They wrap HexEncodingProvider and Base64EncodingProvider, whose TryEncodeAsync and TryDecodeAsync(Stream, Stream) have been genuinely async since PR #22. The obfuscators forward only the synchronous TryEncode(Stream, Stream), so their async path never reaches the async encoder.
Failure scenario
An ASP.NET Core endpoint obfuscates the request body:
Kestrel disallows synchronous I/O by default, so this throws InvalidOperationException: Synchronous operations are disallowed. HexEncodingProvider.TryEncodeAsync succeeds on the same stream. With a stream that throws NotSupportedException from its synchronous members, such as the repo's own AsyncOnlyStream test double, the obfuscators catch the exception and return false without saying why.
Repro at 89fc373, using a stream whose ReadAsync works and whose Read throws as Kestrel does:
HexEncodingProvider.TryEncodeAsync: True -> '68656C6C6F'
HexObfuscationProvider.TryObfuscateAsync: threw InvalidOperationException: Synchronous operations are disallowed.
Base64ObfuscationProvider.TryObfuscateAsync: threw InvalidOperationException: Synchronous operations are disallowed.
XorObfuscationProvider.TryObfuscateAsync: threw InvalidOperationException: Synchronous operations are disallowed.
The async-surface spec left obfuscation out because no single generic default suits Hex, Reverse and Base64. That argument is against one shared default. It doesn't stop each provider declaring its own async primitives, which is what the encoders, compressors and AES have done since. StreamChunks already provides the chunk loop that Xor, Caesar and BitRotate share. The change adds API without breaking any, and doesn't depend on #8's remaining ValueTask decision.
Suggested fix
Make the derived stream defaults on IObfuscationProvider (TryObfuscateAsync(ReadOnlyMemory<byte>, Stream), ObfuscateAsync(Stream) and their deobfuscate counterparts) compose over the async primitives, as IEncodingProvider already does. A provider that declares the two Try…Async(Stream, Stream) primitives then converts all six members.
Hex and Base64 obfuscators: forward TryObfuscateAsync and TryDeobfuscateAsync(Stream, Stream, ct) to _encoder.TryEncodeAsync and _encoder.TryDecodeAsync.
Xor, Caesar and BitRotate: add a StreamChunks.ForEachAsync that uses ReadAsync/WriteAsync over the same pooled buffer, and declare the async primitives with it. Xor carries its key offset across chunks, as the sync path does.
Reverse and Composite: buffer with CopyToAsync and write with WriteAsync. They have to hold the whole input either way.
Update README's "Async Support" line and CLAUDE.md's three-tier note to list obfuscation as genuinely async.
Acceptance criteria
Every obfuscator passes the AsyncOnlyStream tests in Essentials.Tests/AsyncStreamIoTests.cs in both directions.
Async output is byte-identical to sync output for every obfuscator, including Xor with a multi-byte key over input longer than StreamChunks.BufferSize.
An already-cancelled token is honoured before any read.
Priority: Medium. The async API throws InvalidOperationException on Kestrel request bodies, the most common real async stream, and on async-only streams it returns false without saying why. The workaround is to buffer first. Hex and Base64 only need forwarding to encoders that are already async.
Area / suggested assignment: The IObfuscationProvider default async members, the Hex/Base64/Xor/Caesar/BitRotate/Reverse/Composite obfuscation providers, and StreamChunks (add a ForEachAsync).
Next step: Do steps 1–2 first: compose the defaults over the async primitives, and forward Hex and Base64 to their encoders. That fixes the Kestrel case for the encoders that already have async support. Then add StreamChunks.ForEachAsync for Xor, Caesar and BitRotate, with the byte-identical sync/async tests.
Activity
matt-edmondson commentedon Nov 2, 2026
Triage
InvalidOperationExceptionon Kestrel request bodies, the most common real async stream, and on async-only streams it returnsfalsewithout saying why. The workaround is to buffer first. Hex and Base64 only need forwarding to encoders that are already async.IObfuscationProviderdefault async members, the Hex/Base64/Xor/Caesar/BitRotate/Reverse/Composite obfuscation providers, andStreamChunks(add aForEachAsync).StreamChunks.ForEachAsyncfor Xor, Caesar and BitRotate, with the byte-identical sync/async tests.Generated by Claude Code