Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ILogger, int, string?, Exception?> _scriptInjectionSkipped = LoggerMessage.Define<int, string?>(
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);
}
}
}
53 changes: 29 additions & 24 deletions src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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("<script src=\"/_framework/aspnetcore-browser-refresh.js\"></script>", 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("<script src=\"/_framework/aspnetcore-browser-refresh.js\"></script>", responseContent);
}

private async Task<string> TestBrowserRefreshMiddleware(int statusCode, string contentType, string content, bool includeHtmlWrapper = true)
{
// Arrange
var stream = new MemoryStream();
Expand All @@ -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("<html>");
await context.Response.WriteAsync("<body>");
await context.Response.WriteAsync("<h1>");
await context.Response.WriteAsync("Hello world");
await context.Response.WriteAsync("</h1>");
await context.Response.WriteAsync("</body>");
await context.Response.WriteAsync("</html>");
if (includeHtmlWrapper)
{
await context.Response.WriteAsync("<html>");
await context.Response.WriteAsync("<body>");
await context.Response.WriteAsync("<h1>");
await context.Response.WriteAsync(content);
await context.Response.WriteAsync("</h1>");
await context.Response.WriteAsync("</body>");
await context.Response.WriteAsync("</html>");
}
else
{
await context.Response.WriteAsync(content);
}
}, NullLogger<BrowserRefreshMiddleware>.Instance);

// Act
await middleware.InvokeAsync(context);

// Assert
// Return response content and verify status code
var responseContent = Encoding.UTF8.GetString(stream.ToArray());
Assert.Equal("<html><body><h1>Hello world</h1><script src=\"/_framework/aspnetcore-browser-refresh.js\"></script></body></html>", responseContent);
Assert.Equal(statusCode, context.Response.StatusCode);
return responseContent;
}

private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature
Expand Down
Loading