The Middleware Bug That Took Down Our API (Twice)

์ž‘์„ฑ์ž

์นดํ…Œ๊ณ ๋ฆฌ:

โ† ํ”ผ๋“œ๋กœ
DEV Community ยท M Nouman Berlas ยท 2026-07-08 ๊ฐœ๋ฐœ(SW)
Cover image for The Middleware Bug That Took Down Our API (Twice)

M Nouman Berlas

Picture this: Your API is humming along perfectly. 99.9% success rate. Life is good.
Then suddenly – BAM! ๐Ÿ”ฅ
Random 500 errors. Garbled responses. Users complaining about “weird characters” in their data.
And it only happens under load.
The Culprit? Our “innocent” logging middleware.
Here’s what we did WRONG:

//
public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
    await next(context); // Call next middleware

    // Try to log the response
    var body = await context.Response.Body.ReadAsStringAsync();
    Logger.Log(body); // Oops...
}
//

Why This Fails Spectacularly:
1.  The response stream is already consumed and sent to the client
2.  Multiple requests try to read/modify streams concurrently
3.  Streams aren't thread-safe by default
4.  Gzip-compressed responses come back as garbage characters
5.  Everything breaks in weird, unpredictable ways
The Fix That Actually Works:

`
public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
    // Step 1: Enable request buffering
    context.Request.EnableBuffering();

    // Step 2: Swap response stream with a buffered one
    var originalBodyStream = context.Response.Body;
    using var memoryStream = new MemoryStream();
    context.Response.Body = memoryStream;

    try {
        // Step 3: Let the pipeline execute
        await next(context);

        // Step 4: Safely read the buffered response
        memoryStream.Position = 0;

        // Step 5: Handle compression properly
        string responseBody;
        if (context.Response.Headers.ContentEncoding.Contains("gzip")) {
            using var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress);
            using var reader = new StreamReader(gzipStream, Encoding.UTF8);
            responseBody = await reader.ReadToEndAsync();
        } else {
            using var reader = new StreamReader(memoryStream, Encoding.UTF8);
            responseBody = await reader.ReadToEndAsync();
        }

        // Step 6: Log it
        Logger.Log(responseBody);

        // Step 7: Copy back to original stream
        memoryStream.Position = 0;
        await memoryStream.CopyToAsync(originalBodyStream);
    } finally {
        // Step 8: Restore original stream
        context.Response.Body = originalBodyStream;
    }
}

Enter fullscreen mode Exit fullscreen mode

The Hidden Gotcha Nobody Tells You:
Modern APIs use gzip/deflate compression by default. If you don’t decompress before reading, you get binary garbage that looks like: ๏ฟฝH๏ฟฝ๏ฟฝ๏ฟฝW๏ฟฝK๏ฟฝM๏ฟฝU๏ฟฝ๏ฟฝZ

Our Results:
โœ… Zero stream-related exceptions
โœ… Zero “garbage character” bug reports
โœ… Proper logging at 10,000+ req/min
โœ… My on-call rotation got way quieter ๐Ÿ˜ด
The Brutal Truth:
Middleware is easy to write. Thread-safe, production-ready middleware? That’s an art form.

Who else has spent hours debugging middleware issues? Let’s commiserate in the comments ๐Ÿ‘‡

dotnet #aspnetcore #middleware #threadsafety #debugging #productionissues #softwareengineering #webdevelopment #csharp #lessonslearned #cloudnative

P.S. Always test middleware under load. Your local machine running 1 request at a time will NEVER expose these bugs.

์›๋ฌธ์—์„œ ๊ณ„์† โ†—

์ถ”์ถœ ๋ณธ๋ฌธ ยท ์ถœ์ฒ˜: dev.to ยท https://dev.to/noumanberlas/the-middleware-bug-that-took-down-our-api-twice-4mej

์ฝ”๋ฉ˜ํŠธ

๋‹ต๊ธ€ ๋‚จ๊ธฐ๊ธฐ

์ด๋ฉ”์ผ ์ฃผ์†Œ๋Š” ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•„๋“œ๋Š” *๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค