Skip to content

Commit f338f70

Browse files
github-actions[bot]MackinnonBuckjaviercn
authored
[release/10.0.1xx] [BrowserRefresh] Handle gzip-compressed responses in ResponseStreamWrapper (#50535)
Co-authored-by: Mackinnon Buck <[email protected]> Co-authored-by: javiercn <[email protected]> Co-authored-by: Javier Calvarro Nelson <[email protected]>
1 parent da24ea8 commit f338f70

File tree

8 files changed

+1413
-302
lines changed

8 files changed

+1413
-302
lines changed

src/BuiltInTools/BrowserRefresh/BrowserRefreshMiddleware.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public async Task InvokeAsync(HttpContext context)
4646
try
4747
{
4848
await _next(context);
49+
50+
// We complete the wrapper stream to ensure that any intermediate buffers
51+
// get fully flushed to the response stream. This is also required to
52+
// reliably determine whether script injection was performed.
53+
await responseStreamWrapper.CompleteAsync();
4954
}
5055
finally
5156
{
@@ -219,7 +224,7 @@ internal static class Log
219224
LogLevel.Warning,
220225
new EventId(3, "FailedToConfiguredForRefreshes"),
221226
"Unable to configure browser refresh script injection on the response. " +
222-
$"Consider manually adding '{WebSocketScriptInjection.InjectedScript}' to the body of the page.");
227+
$"Consider manually adding '{ScriptInjectingStream.InjectedScript}' to the body of the page.");
223228

224229
private static readonly Action<ILogger, StringValues, Exception?> _responseCompressionDetected = LoggerMessage.Define<StringValues>(
225230
LogLevel.Warning,

src/BuiltInTools/BrowserRefresh/BrowserScriptMiddleware.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal static ReadOnlyMemory<byte> GetBlazorHotReloadJS()
4444
{
4545
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.BlazorHotReload.js";
4646
using var stream = new MemoryStream();
47-
var manifestStream = typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!;
47+
var manifestStream = typeof(BrowserScriptMiddleware).Assembly.GetManifestResourceStream(jsFileName)!;
4848
manifestStream.CopyTo(stream);
4949

5050
return stream.ToArray();
@@ -61,7 +61,7 @@ internal static ReadOnlyMemory<byte> GetBrowserRefreshJS()
6161
internal static ReadOnlyMemory<byte> GetWebSocketClientJavaScript(string hostString, string serverKey)
6262
{
6363
var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.WebSocketScriptInjection.js";
64-
using var reader = new StreamReader(typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!);
64+
using var reader = new StreamReader(typeof(BrowserScriptMiddleware).Assembly.GetManifestResourceStream(jsFileName)!);
6565
var script = reader.ReadToEnd()
6666
.Replace("{{hostString}}", hostString)
6767
.Replace("{{ServerKey}}", serverKey);
Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
// Based on https://github.com/RickStrahl/Westwind.AspnetCore.LiveReload/blob/128b5f524e86954e997f2c453e7e5c1dcc3db746/Westwind.AspnetCore.LiveReload/ResponseStreamWrapper.cs
5-
4+
using System.Diagnostics;
5+
using System.IO.Compression;
6+
using System.IO.Pipelines;
67
using Microsoft.AspNetCore.Http;
78
using Microsoft.Extensions.Logging;
89
using Microsoft.Net.Http.Headers;
@@ -15,12 +16,18 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh
1516
/// </summary>
1617
public class ResponseStreamWrapper : Stream
1718
{
18-
private static readonly MediaTypeHeaderValue _textHtmlMediaType = new("text/html");
19-
private readonly Stream _baseStream;
19+
private static readonly MediaTypeHeaderValue s_textHtmlMediaType = new("text/html");
20+
2021
private readonly HttpContext _context;
2122
private readonly ILogger _logger;
2223
private bool? _isHtmlResponse;
2324

25+
private Stream _baseStream;
26+
private ScriptInjectingStream? _scriptInjectingStream;
27+
private Pipe? _pipe;
28+
private Task? _gzipCopyTask;
29+
private bool _disposed;
30+
2431
public ResponseStreamWrapper(HttpContext context, ILogger logger)
2532
{
2633
_context = context;
@@ -33,33 +40,25 @@ public ResponseStreamWrapper(HttpContext context, ILogger logger)
3340
public override bool CanWrite => true;
3441
public override long Length { get; }
3542
public override long Position { get; set; }
36-
public bool ScriptInjectionPerformed { get; private set; }
37-
38-
public bool IsHtmlResponse => _isHtmlResponse ?? false;
43+
public bool ScriptInjectionPerformed => _scriptInjectingStream?.ScriptInjectionPerformed == true;
44+
public bool IsHtmlResponse => _isHtmlResponse == true;
3945

4046
public override void Flush()
4147
{
4248
OnWrite();
4349
_baseStream.Flush();
4450
}
4551

46-
public override Task FlushAsync(CancellationToken cancellationToken)
52+
public override async Task FlushAsync(CancellationToken cancellationToken)
4753
{
4854
OnWrite();
49-
return _baseStream.FlushAsync(cancellationToken);
55+
await _baseStream.FlushAsync(cancellationToken);
5056
}
5157

5258
public override void Write(ReadOnlySpan<byte> buffer)
5359
{
5460
OnWrite();
55-
if (IsHtmlResponse && !ScriptInjectionPerformed)
56-
{
57-
ScriptInjectionPerformed = WebSocketScriptInjection.TryInjectLiveReloadScript(_baseStream, buffer);
58-
}
59-
else
60-
{
61-
_baseStream.Write(buffer);
62-
}
61+
_baseStream.Write(buffer);
6362
}
6463

6564
public override void WriteByte(byte value)
@@ -71,43 +70,19 @@ public override void WriteByte(byte value)
7170
public override void Write(byte[] buffer, int offset, int count)
7271
{
7372
OnWrite();
74-
75-
if (IsHtmlResponse && !ScriptInjectionPerformed)
76-
{
77-
ScriptInjectionPerformed = WebSocketScriptInjection.TryInjectLiveReloadScript(_baseStream, buffer.AsSpan(offset, count));
78-
}
79-
else
80-
{
81-
_baseStream.Write(buffer, offset, count);
82-
}
73+
_baseStream.Write(buffer.AsSpan(offset, count));
8374
}
8475

8576
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
8677
{
8778
OnWrite();
88-
89-
if (IsHtmlResponse && !ScriptInjectionPerformed)
90-
{
91-
ScriptInjectionPerformed = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(_baseStream, buffer.AsMemory(offset, count), cancellationToken);
92-
}
93-
else
94-
{
95-
await _baseStream.WriteAsync(buffer, offset, count, cancellationToken);
96-
}
79+
await _baseStream.WriteAsync(buffer.AsMemory(offset, count), cancellationToken);
9780
}
9881

9982
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
10083
{
10184
OnWrite();
102-
103-
if (IsHtmlResponse && !ScriptInjectionPerformed)
104-
{
105-
ScriptInjectionPerformed = await WebSocketScriptInjection.TryInjectLiveReloadScriptAsync(_baseStream, buffer, cancellationToken);
106-
}
107-
else
108-
{
109-
await _baseStream.WriteAsync(buffer, cancellationToken);
110-
}
85+
await _baseStream.WriteAsync(buffer, cancellationToken);
11186
}
11287

11388
private void OnWrite()
@@ -122,15 +97,38 @@ private void OnWrite()
12297
_isHtmlResponse =
12398
(response.StatusCode == StatusCodes.Status200OK || response.StatusCode == StatusCodes.Status500InternalServerError) &&
12499
MediaTypeHeaderValue.TryParse(response.ContentType, out var mediaType) &&
125-
mediaType.IsSubsetOf(_textHtmlMediaType) &&
100+
mediaType.IsSubsetOf(s_textHtmlMediaType) &&
126101
(!mediaType.Charset.HasValue || mediaType.Charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase));
127102

128103
if (_isHtmlResponse.Value)
129104
{
130105
BrowserRefreshMiddleware.Log.SetupResponseForBrowserRefresh(_logger);
131-
132106
// Since we're changing the markup content, reset the content-length
133107
response.Headers.ContentLength = null;
108+
109+
_scriptInjectingStream = new ScriptInjectingStream(_baseStream);
110+
111+
// By default, write directly to the script injection stream.
112+
// We may change the base stream below if we detect that the response
113+
// is compressed.
114+
_baseStream = _scriptInjectingStream;
115+
116+
// Check if the response has gzip Content-Encoding
117+
if (response.Headers.TryGetValue(HeaderNames.ContentEncoding, out var contentEncodingValues))
118+
{
119+
var contentEncoding = contentEncodingValues.FirstOrDefault();
120+
if (string.Equals(contentEncoding, "gzip", StringComparison.OrdinalIgnoreCase))
121+
{
122+
// Remove the Content-Encoding header since we'll be serving uncompressed content
123+
response.Headers.Remove(HeaderNames.ContentEncoding);
124+
125+
_pipe = new Pipe();
126+
var gzipStream = new GZipStream(_pipe.Reader.AsStream(leaveOpen: true), CompressionMode.Decompress, leaveOpen: true);
127+
128+
_gzipCopyTask = gzipStream.CopyToAsync(_scriptInjectingStream);
129+
_baseStream = _pipe.Writer.AsStream(leaveOpen: true);
130+
}
131+
}
134132
}
135133
}
136134

@@ -145,5 +143,45 @@ public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken
145143
=> throw new NotSupportedException();
146144

147145
public override void SetLength(long value) => throw new NotSupportedException();
146+
147+
protected override void Dispose(bool disposing)
148+
{
149+
if (disposing)
150+
{
151+
DisposeAsync().AsTask().GetAwaiter().GetResult();
152+
}
153+
}
154+
155+
public ValueTask CompleteAsync() => DisposeAsync();
156+
157+
public override async ValueTask DisposeAsync()
158+
{
159+
if (_disposed)
160+
{
161+
return;
162+
}
163+
164+
_disposed = true;
165+
166+
if (_pipe is not null)
167+
{
168+
await _pipe.Writer.CompleteAsync();
169+
}
170+
171+
if (_gzipCopyTask is not null)
172+
{
173+
await _gzipCopyTask;
174+
}
175+
176+
if (_scriptInjectingStream is not null)
177+
{
178+
await _scriptInjectingStream.CompleteAsync();
179+
}
180+
else
181+
{
182+
Debug.Assert(_isHtmlResponse != true);
183+
await _baseStream.FlushAsync();
184+
}
185+
}
148186
}
149187
}

0 commit comments

Comments
 (0)