Skip to content

Commit 7d4ae9a

Browse files
Hot reload script injection improvements (#27537)
1 parent 8aa0d11 commit 7d4ae9a

File tree

6 files changed

+192
-272
lines changed

6 files changed

+192
-272
lines changed

src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.IO;
68
using System.Threading.Tasks;
79
using Microsoft.AspNetCore.Http;
810
using Microsoft.AspNetCore.Http.Features;
@@ -26,10 +28,10 @@ public async Task InvokeAsync(HttpContext context)
2628
// We only need to support this for requests that could be initiated by a browser.
2729
if (IsBrowserDocumentRequest(context))
2830
{
29-
// Use a custom StreamWrapper to rewrite output on Write/WriteAsync
30-
using var responseStreamWrapper = new ResponseStreamWrapper(context, _logger);
31+
// Use a custom stream to buffer the response body for rewriting.
32+
using var memoryStream = new MemoryStream();
3133
var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
32-
context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(responseStreamWrapper));
34+
context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(memoryStream));
3335

3436
try
3537
{
@@ -40,15 +42,35 @@ public async Task InvokeAsync(HttpContext context)
4042
context.Features.Set(originalBodyFeature);
4143
}
4244

43-
if (responseStreamWrapper.IsHtmlResponse && _logger.IsEnabled(LogLevel.Debug))
45+
if (memoryStream.TryGetBuffer(out var buffer) && buffer.Count > 0)
4446
{
45-
if (responseStreamWrapper.ScriptInjectionPerformed)
47+
var response = context.Response;
48+
var baseStream = response.Body;
49+
50+
if (IsHtmlResponse(response))
4651
{
47-
Log.BrowserConfiguredForRefreshes(_logger);
52+
Log.SetupResponseForBrowserRefresh(_logger);
53+
54+
// Since we're changing the markup content, reset the content-length
55+
response.Headers.ContentLength = null;
56+
57+
var scriptInjectionPerformed = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(baseStream, buffer);
58+
if (scriptInjectionPerformed)
59+
{
60+
Log.BrowserConfiguredForRefreshes(_logger);
61+
}
62+
else if (response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var contentEncodings))
63+
{
64+
Log.ResponseCompressionDetected(_logger, contentEncodings);
65+
}
66+
else
67+
{
68+
Log.FailedToConfiguredForRefreshes(_logger);
69+
}
4870
}
4971
else
5072
{
51-
Log.FailedToConfiguredForRefreshes(_logger);
73+
await baseStream.WriteAsync(buffer);
5274
}
5375
}
5476
}
@@ -92,26 +114,41 @@ internal static bool IsBrowserDocumentRequest(HttpContext context)
92114
return false;
93115
}
94116

117+
private bool IsHtmlResponse(HttpResponse response)
118+
=> (response.StatusCode == StatusCodes.Status200OK || response.StatusCode == StatusCodes.Status500InternalServerError) &&
119+
MediaTypeHeaderValue.TryParse(response.ContentType, out var mediaType) &&
120+
mediaType.IsSubsetOf(_textHtmlMediaType) &&
121+
(!mediaType.Charset.HasValue || mediaType.Charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase));
122+
95123
internal static class Log
96124
{
97125
private static readonly Action<ILogger, Exception?> _setupResponseForBrowserRefresh = LoggerMessage.Define(
98-
LogLevel.Debug,
126+
LogLevel.Debug,
99127
new EventId(1, "SetUpResponseForBrowserRefresh"),
100-
"Response markup is scheduled to include browser refresh script injection.");
128+
"Response markup is scheduled to include browser refresh script injection.");
101129

102130
private static readonly Action<ILogger, Exception?> _browserConfiguredForRefreshes = LoggerMessage.Define(
103-
LogLevel.Debug,
131+
LogLevel.Debug,
104132
new EventId(2, "BrowserConfiguredForRefreshes"),
105-
"Response markup was updated to include browser refresh script injection.");
133+
"Response markup was updated to include browser refresh script injection.");
106134

107135
private static readonly Action<ILogger, Exception?> _failedToConfigureForRefreshes = LoggerMessage.Define(
108-
LogLevel.Debug,
136+
LogLevel.Warning,
109137
new EventId(3, "FailedToConfiguredForRefreshes"),
110-
"Unable to configure browser refresh script injection on the response.");
138+
"Unable to configure browser refresh script injection on the response. " +
139+
$"Consider manually adding '{WebSocketScriptInjection.InjectedScript}' to the body of the page.");
140+
141+
private static readonly Action<ILogger, StringValues, Exception?> _responseCompressionDetected = LoggerMessage.Define<StringValues>(
142+
LogLevel.Warning,
143+
new EventId(4, "ResponseCompressionDetected"),
144+
"Unable to configure browser refresh script injection on the response. " +
145+
$"This may have been caused by the response's {HeaderNames.ContentEncoding}: '{{encoding}}'. " +
146+
"Consider disabling response compression.");
111147

112148
public static void SetupResponseForBrowserRefresh(ILogger logger) => _setupResponseForBrowserRefresh(logger, null);
113149
public static void BrowserConfiguredForRefreshes(ILogger logger) => _browserConfiguredForRefreshes(logger, null);
114150
public static void FailedToConfiguredForRefreshes(ILogger logger) => _failedToConfigureForRefreshes(logger, null);
151+
public static void ResponseCompressionDetected(ILogger logger, StringValues encoding) => _responseCompressionDetected(logger, encoding, null);
115152
}
116153
}
117154
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace System;
5+
6+
internal static class ReadOnlySpanOfByteExtensions
7+
{
8+
public static int LastIndexOfNonWhiteSpace(this ReadOnlySpan<byte> buffer)
9+
{
10+
for (var i = buffer.Length - 1; i >= 0; i--)
11+
{
12+
if (!char.IsWhiteSpace(Convert.ToChar(buffer[i])))
13+
{
14+
return i;
15+
}
16+
}
17+
18+
return -1;
19+
}
20+
21+
public static bool EndsWithIgnoreCase(this ReadOnlySpan<byte> buffer, ReadOnlySpan<byte> value)
22+
{
23+
if (buffer.Length < value.Length)
24+
{
25+
return false;
26+
}
27+
28+
for (var i = 1; i <= value.Length; i++)
29+
{
30+
if (char.ToLowerInvariant(Convert.ToChar(value[^i])) != char.ToLowerInvariant(Convert.ToChar(buffer[^i])))
31+
{
32+
return false;
33+
}
34+
}
35+
36+
return true;
37+
}
38+
}

src/BuiltInTools/BrowserRefresh/ResponseStreamWrapper.cs

Lines changed: 0 additions & 153 deletions
This file was deleted.

0 commit comments

Comments
 (0)