From 0644d4984d92cba96923ce97c611417dba91b596 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 26 Sep 2025 17:08:38 +0200 Subject: [PATCH] Allow 404 status code, log when no injection made and add unit test. --- .../BrowserRefreshMiddleware.cs | 6 ++ .../BrowserRefresh/ResponseStreamWrapper.cs | 53 +++++++++-------- .../BrowserRefreshMiddlewareTest.cs | 57 ++++++++++++++----- 3 files changed, 79 insertions(+), 37 deletions(-) diff --git a/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs b/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs index 853b52a83778..13fac3ceae8e 100644 --- a/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs +++ b/src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs @@ -233,10 +233,16 @@ internal static class Log $"This may have been caused by the response's {HeaderNames.ContentEncoding}: '{{encoding}}'. " + "Consider disabling response compression."); + private static readonly Action _scriptInjectionSkipped = LoggerMessage.Define( + LogLevel.Debug, + new EventId(6, "ScriptInjectionSkipped"), + "Browser refresh script injection skipped. Status code: {StatusCode}, Content type: {ContentType}"); + public static void SetupResponseForBrowserRefresh(ILogger logger) => _setupResponseForBrowserRefresh(logger, null); public static void BrowserConfiguredForRefreshes(ILogger logger) => _browserConfiguredForRefreshes(logger, null); public static void FailedToConfiguredForRefreshes(ILogger logger) => _failedToConfigureForRefreshes(logger, null); public static void ResponseCompressionDetected(ILogger logger, StringValues encoding) => _responseCompressionDetected(logger, encoding, null); + public static void ScriptInjectionSkipped(ILogger logger, int statusCode, string? contentType) => _scriptInjectionSkipped(logger, statusCode, contentType, null); } } } diff --git a/src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs b/src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs index e76ced8d7a2d..16b2256e3f8d 100644 --- a/src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs +++ b/src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs @@ -95,39 +95,44 @@ private void OnWrite() var response = _context.Response; _isHtmlResponse = - (response.StatusCode == StatusCodes.Status200OK || response.StatusCode == StatusCodes.Status500InternalServerError) && + (response.StatusCode == StatusCodes.Status200OK || + response.StatusCode == StatusCodes.Status404NotFound || + response.StatusCode == StatusCodes.Status500InternalServerError) && MediaTypeHeaderValue.TryParse(response.ContentType, out var mediaType) && mediaType.IsSubsetOf(s_textHtmlMediaType) && (!mediaType.Charset.HasValue || mediaType.Charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)); - if (_isHtmlResponse.Value) + if (!_isHtmlResponse.Value) { - BrowserRefreshMiddleware.Log.SetupResponseForBrowserRefresh(_logger); - // Since we're changing the markup content, reset the content-length - response.Headers.ContentLength = null; + BrowserRefreshMiddleware.Log.ScriptInjectionSkipped(_logger, response.StatusCode, response.ContentType); + return; + } + + BrowserRefreshMiddleware.Log.SetupResponseForBrowserRefresh(_logger); + // Since we're changing the markup content, reset the content-length + response.Headers.ContentLength = null; - _scriptInjectingStream = new ScriptInjectingStream(_baseStream); + _scriptInjectingStream = new ScriptInjectingStream(_baseStream); - // By default, write directly to the script injection stream. - // We may change the base stream below if we detect that the response - // is compressed. - _baseStream = _scriptInjectingStream; + // By default, write directly to the script injection stream. + // We may change the base stream below if we detect that the response + // is compressed. + _baseStream = _scriptInjectingStream; - // Check if the response has gzip Content-Encoding - if (response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var contentEncodingValues)) + // Check if the response has gzip Content-Encoding + if (response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var contentEncodingValues)) + { + var contentEncoding = contentEncodingValues.FirstOrDefault(); + if (string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase)) { - var contentEncoding = contentEncodingValues.FirstOrDefault(); - if (string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase)) - { - // Remove the Content-Encoding header since we'll be serving uncompressed content - response.Headers.Remove(HeaderNames.ContentEncoding); - - _pipe = new Pipe(); - var gzipStream = new GZipStream(_pipe.Reader.AsStream(leaveOpen: true), CompressionMode.Decompress, leaveOpen: true); - - _gzipCopyTask = gzipStream.CopyToAsync(_scriptInjectingStream); - _baseStream = _pipe.Writer.AsStream(leaveOpen: true); - } + // Remove the Content-Encoding header since we'll be serving uncompressed content + response.Headers.Remove(HeaderNames.ContentEncoding); + + _pipe = new Pipe(); + var gzipStream = new GZipStream(_pipe.Reader.AsStream(leaveOpen: true), CompressionMode.Decompress, leaveOpen: true); + + _gzipCopyTask = gzipStream.CopyToAsync(_scriptInjectingStream); + _baseStream = _pipe.Writer.AsStream(leaveOpen: true); } } } diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs index 1fa4b862b693..7dc38ed6d896 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs @@ -3,6 +3,7 @@ using System.IO.Pipelines; using System.Runtime.CompilerServices; +using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging.Abstractions; @@ -555,8 +556,30 @@ public async Task InvokeAsync_DoesNotAttachHeaders_WhenAlreadyAttached() Assert.Equal("true", context.Response.Headers["ASPNETCORE-BROWSER-TOOLS"]); } - [Fact] - public async Task InvokeAsync_AddsScriptToThePage() + [Theory] + [InlineData(500, "text/html")] + [InlineData(404, "text/html")] + [InlineData(200, "text/html")] + public async Task InvokeAsync_AddsScriptToThePage_ForSupportedStatusCodes(int statusCode, string contentType) + { + // Act & Assert + var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content"); + Assert.Contains("", responseContent); + } + + [Theory] + [InlineData(400, "text/html")] // Bad Request + [InlineData(401, "text/html")] // Unauthorized + [InlineData(404, "application/json")] // 404 with wrong content type + [InlineData(200, "application/json")] // 200 with wrong content type + public async Task InvokeAsync_DoesNotAddScript_ForUnsupportedStatusCodesOrContentTypes(int statusCode, string contentType) + { + // Act & Assert + var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content", includeHtmlWrapper: false); + Assert.DoesNotContain("", responseContent); + } + + private async Task TestBrowserRefreshMiddleware(int statusCode, string contentType, string content, bool includeHtmlWrapper = true) { // Arrange var stream = new MemoryStream(); @@ -575,24 +598,32 @@ public async Task InvokeAsync_AddsScriptToThePage() var middleware = new BrowserRefreshMiddleware(async (context) => { + context.Response.StatusCode = statusCode; + context.Response.ContentType = contentType; - context.Response.ContentType = "text/html"; - - await context.Response.WriteAsync(""); - await context.Response.WriteAsync(""); - await context.Response.WriteAsync("

"); - await context.Response.WriteAsync("Hello world"); - await context.Response.WriteAsync("

"); - await context.Response.WriteAsync(""); - await context.Response.WriteAsync(""); + if (includeHtmlWrapper) + { + await context.Response.WriteAsync(""); + await context.Response.WriteAsync(""); + await context.Response.WriteAsync("

"); + await context.Response.WriteAsync(content); + await context.Response.WriteAsync("

"); + await context.Response.WriteAsync(""); + await context.Response.WriteAsync(""); + } + else + { + await context.Response.WriteAsync(content); + } }, NullLogger.Instance); // Act await middleware.InvokeAsync(context); - // Assert + // Return response content and verify status code var responseContent = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal("

Hello world

", responseContent); + Assert.Equal(statusCode, context.Response.StatusCode); + return responseContent; } private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature