Skip to content

Commit e2f8031

Browse files
For HMR, proxy all requests including /__webpack_hmr. Fixes aspnet#271.
1 parent 2cffab1 commit e2f8031

File tree

4 files changed

+49
-30
lines changed

4 files changed

+49
-30
lines changed

src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public ConditionalProxyMiddleware(
3030
_pathPrefix = pathPrefix;
3131
_options = options;
3232
_httpClient = new HttpClient(new HttpClientHandler());
33+
_httpClient.Timeout = _options.RequestTimeout;
3334
}
3435

3536
public async Task Invoke(HttpContext context)
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1+
using System;
2+
13
namespace Microsoft.AspNetCore.SpaServices.Webpack
24
{
35
internal class ConditionalProxyMiddlewareOptions
46
{
5-
public ConditionalProxyMiddlewareOptions(string scheme, string host, string port)
7+
public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout)
68
{
79
Scheme = scheme;
810
Host = host;
911
Port = port;
12+
RequestTimeout = requestTimeout;
1013
}
1114

1215
public string Scheme { get; }
1316
public string Host { get; }
1417
public string Port { get; }
18+
public TimeSpan RequestTimeout { get; }
1519
}
1620
}

src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using System;
22
using System.IO;
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using Microsoft.AspNetCore.NodeServices;
56
using Microsoft.AspNetCore.SpaServices.Webpack;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.AspNetCore.Hosting;
89
using Microsoft.Extensions.PlatformAbstractions;
910
using Newtonsoft.Json;
11+
using Microsoft.AspNetCore.Http;
1012

1113
namespace Microsoft.AspNetCore.Builder
1214
{
@@ -15,8 +17,6 @@ namespace Microsoft.AspNetCore.Builder
1517
/// </summary>
1618
public static class WebpackDevMiddleware
1719
{
18-
private const string WebpackDevMiddlewareScheme = "http";
19-
private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr";
2020
private const string DefaultConfigFile = "webpack.config.js";
2121

2222
/// <summary>
@@ -75,12 +75,18 @@ public static void UseWebpackDevMiddleware(
7575
"/Content/Node/webpack-dev-middleware.js");
7676
var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
7777

78+
// Ideally, this would be relative to the application's PathBase (so it could work in virtual directories)
79+
// but it's not clear that such information exists during application startup, as opposed to within the context
80+
// of a request.
81+
var hmrEndpoint = "/__webpack_hmr";
82+
7883
// Tell Node to start the server hosting webpack-dev-middleware
7984
var devServerOptions = new
8085
{
8186
webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile),
8287
suppliedOptions = options,
83-
understandsMultiplePublicPaths = true
88+
understandsMultiplePublicPaths = true,
89+
hotModuleReplacementEndpointUrl = hmrEndpoint
8490
};
8591
var devServerInfo =
8692
nodeServices.InvokeExportAsync<WebpackDevServerInfo>(nodeScript.FileName, "createWebpackDevServer",
@@ -94,33 +100,30 @@ public static void UseWebpackDevMiddleware(
94100
}
95101

96102
// Proxy the corresponding requests through ASP.NET and into the Node listener
103+
// Anything under /<publicpath> (e.g., /dist) is proxied as a normal HTTP request with a typical timeout (100s is the default from HttpClient),
104+
// plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request).
105+
foreach (var publicPath in devServerInfo.PublicPaths)
106+
{
107+
appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100));
108+
}
109+
appBuilder.UseProxyToLocalWebpackDevMiddleware(hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan);
110+
}
111+
112+
private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout)
113+
{
97114
// Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the
98115
// server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is
99116
// the one making the internal HTTP requests, and it's going to be to some port on this machine
100117
// because aspnet-webpack hosts the dev server there. We can't use the hostname that the client
101118
// sees, because that could be anything (e.g., some upstream load balancer) and we might not be
102119
// able to make outbound requests to it from here.
103-
var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme,
104-
"localhost", devServerInfo.Port.ToString());
105-
foreach (var publicPath in devServerInfo.PublicPaths)
106-
{
107-
appBuilder.UseMiddleware<ConditionalProxyMiddleware>(publicPath, proxyOptions);
108-
}
109-
110-
// While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream,
111-
// and the Microsoft.AspNetCore.Proxy code doesn't handle that entirely - it throws an exception after
112-
// a while. So, just serve a 302 for those. But note that we must use the hostname that the client
113-
// sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker).
114-
appBuilder.Map(WebpackHotMiddlewareEndpoint, builder =>
115-
{
116-
builder.Use(next => ctx =>
117-
{
118-
var hostname = ctx.Request.Host.Host;
119-
ctx.Response.Redirect(
120-
$"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}");
121-
return Task.FromResult(0);
122-
});
123-
});
120+
// Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS,
121+
// because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic
122+
// to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have
123+
// the necessary certificate).
124+
var proxyOptions = new ConditionalProxyMiddlewareOptions(
125+
"http", "localhost", proxyToPort.ToString(), requestTimeout);
126+
appBuilder.UseMiddleware<ConditionalProxyMiddleware>(publicPath, proxyOptions);
124127
}
125128

126129
#pragma warning disable CS0649

src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface CreateDevServerCallback {
1919
interface CreateDevServerOptions {
2020
webpackConfigPath: string;
2121
suppliedOptions: DevServerOptions;
22+
hotModuleReplacementEndpointUrl: string;
2223
}
2324

2425
// These are the options configured in C# and then JSON-serialized, hence the C#-style naming
@@ -28,7 +29,7 @@ interface DevServerOptions {
2829
ReactHotModuleReplacement: boolean;
2930
}
3031

31-
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrEndpoint: string) {
32+
function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrClientEndpoint: string, hmrServerEndpoint: string) {
3233
// Build the final Webpack config based on supplied options
3334
if (enableHotModuleReplacement) {
3435
// For this, we only support the key/value config format, not string or string[], since
@@ -44,7 +45,7 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
4445
// Augment all entry points so they support HMR (unless they already do)
4546
Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => {
4647
const webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client';
47-
const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrEndpoint);
48+
const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrClientEndpoint);
4849
if (typeof entryPoints[entryPointName] === 'string') {
4950
entryPoints[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints[entryPointName]];
5051
} else if (firstIndexOfStringStartingWith(entryPoints[entryPointName], webpackHotMiddlewareEntryPoint) < 0) {
@@ -117,7 +118,9 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati
117118
} catch (ex) {
118119
throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack);
119120
}
120-
app.use(webpackHotMiddlewareModule(compiler));
121+
app.use(webpackHotMiddlewareModule(compiler, {
122+
path: hmrServerEndpoint
123+
}));
121124
}
122125
}
123126

@@ -198,8 +201,16 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
198201
}
199202
normalizedPublicPaths.push(removeTrailingSlash(publicPath));
200203

201-
const hmrEndpoint = `http://localhost:${listener.address().port}/__webpack_hmr`;
202-
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrEndpoint);
204+
// Newer versions of Microsoft.AspNetCore.SpaServices will explicitly pass an HMR endpoint URL
205+
// (because it's relative to the app's URL space root, which the client doesn't otherwise know).
206+
// For back-compatibility, fall back on connecting directly to the underlying HMR server (though
207+
// that won't work if the app is hosted on HTTPS because of the mixed-content rule, and we can't
208+
// run the HMR server itself on HTTPS because in general it has no valid cert).
209+
const hmrClientEndpoint = options.hotModuleReplacementEndpointUrl // The URL that we'll proxy (e.g., /__asp_webpack_hmr)
210+
|| `http://localhost:${listener.address().port}/__webpack_hmr`; // Fall back on absolute URL to bypass proxying
211+
const hmrServerEndpoint = options.hotModuleReplacementEndpointUrl
212+
|| '/__webpack_hmr'; // URL is relative to webpack dev server root
213+
attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientEndpoint, hmrServerEndpoint);
203214
}
204215
});
205216

0 commit comments

Comments
 (0)