Skip to content
Merged
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed in the PR, but do we want to dispose this stream at some point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could store it same way as _baseStream and use the existing disposal methods to clean up after it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging to fix it asap.


_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