diff --git a/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs b/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs index d7bf85c..66bf99c 100644 --- a/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs +++ b/src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Diagnostics; using System.Text; using System.Text.Json; @@ -279,14 +280,18 @@ public sealed class AuditWriteMiddleware return (null, false); } + // Read AT MOST cap + 1 bytes — the extra byte tells us the body was + // over the cap without forcing us to allocate the whole payload. Rent + // the scratch buffer from the shared ArrayPool so we don't allocate + // (and immediately discard) `cap + 1` bytes per request — the pool + // may hand back a buffer LARGER than `limit`, so we treat `limit` + // (not `buffer.Length`) as the read ceiling. + var limit = capBytes + 1; + var buffer = ArrayPool.Shared.Rent(limit); try { request.Body.Position = 0; - // Read AT MOST cap + 1 bytes — the extra byte tells us the body was - // over the cap without forcing us to allocate the whole payload. - var limit = capBytes + 1; - var buffer = new byte[limit]; var total = 0; while (total < limit) { @@ -299,7 +304,6 @@ public sealed class AuditWriteMiddleware } total += read; } - request.Body.Position = 0; if (total == 0) { @@ -318,6 +322,15 @@ public sealed class AuditWriteMiddleware // outcome. return (null, false); } + finally + { + // Even on a thrown read, the downstream handler must see the full + // body from position 0 — never let a failed audit copy leak a + // truncated view. A rewind failure is swallowed: best-effort, + // same philosophy as the rest of the file. + try { request.Body.Position = 0; } catch { /* swallow */ } + ArrayPool.Shared.Return(buffer); + } } ///