diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 72b66c3..5a56c81 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack _pathPrefix = pathPrefix; _options = options; _httpClient = new HttpClient(new HttpClientHandler()); + _httpClient.Timeout = _options.RequestTimeout; } public async Task Invoke(HttpContext context) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs index 5654007..2c3311a 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -1,16 +1,20 @@ +using System; + namespace Microsoft.AspNetCore.SpaServices.Webpack { internal class ConditionalProxyMiddlewareOptions { - public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout) { Scheme = scheme; Host = host; Port = port; + RequestTimeout = requestTimeout; } public string Scheme { get; } public string Host { get; } public string Port { get; } + public TimeSpan RequestTimeout { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 8a9e432..3b5bb72 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.SpaServices.Webpack; @@ -7,6 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder { @@ -15,8 +17,6 @@ namespace Microsoft.AspNetCore.Builder /// public static class WebpackDevMiddleware { - private const string WebpackDevMiddlewareScheme = "http"; - private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; private const string DefaultConfigFile = "webpack.config.js"; /// @@ -75,12 +75,18 @@ namespace Microsoft.AspNetCore.Builder "/Content/Node/webpack-dev-middleware.js"); var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + // Ideally, this would be relative to the application's PathBase (so it could work in virtual directories) + // but it's not clear that such information exists during application startup, as opposed to within the context + // of a request. + var hmrEndpoint = "/__webpack_hmr"; + // Tell Node to start the server hosting webpack-dev-middleware var devServerOptions = new { webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options, - understandsMultiplePublicPaths = true + understandsMultiplePublicPaths = true, + hotModuleReplacementEndpointUrl = hmrEndpoint }; var devServerInfo = nodeServices.InvokeExportAsync(nodeScript.FileName, "createWebpackDevServer", @@ -94,33 +100,30 @@ namespace Microsoft.AspNetCore.Builder } // Proxy the corresponding requests through ASP.NET and into the Node listener + // Anything under / (e.g., /dist) is proxied as a normal HTTP request with a typical timeout (100s is the default from HttpClient), + // plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request). + foreach (var publicPath in devServerInfo.PublicPaths) + { + appBuilder.UseProxyToLocalWebpackDevMiddleware(publicPath, devServerInfo.Port, TimeSpan.FromSeconds(100)); + } + appBuilder.UseProxyToLocalWebpackDevMiddleware(hmrEndpoint, devServerInfo.Port, Timeout.InfiniteTimeSpan); + } + + private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout) + { // Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the // server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is // the one making the internal HTTP requests, and it's going to be to some port on this machine // because aspnet-webpack hosts the dev server there. We can't use the hostname that the client // sees, because that could be anything (e.g., some upstream load balancer) and we might not be // able to make outbound requests to it from here. - var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, - "localhost", devServerInfo.Port.ToString()); - foreach (var publicPath in devServerInfo.PublicPaths) - { - appBuilder.UseMiddleware(publicPath, proxyOptions); - } - - // While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream, - // and the Microsoft.AspNetCore.Proxy code doesn't handle that entirely - it throws an exception after - // a while. So, just serve a 302 for those. But note that we must use the hostname that the client - // sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker). - appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => - { - builder.Use(next => ctx => - { - var hostname = ctx.Request.Host.Host; - ctx.Response.Redirect( - $"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); - return Task.FromResult(0); - }); - }); + // Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS, + // because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic + // to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have + // the necessary certificate). + var proxyOptions = new ConditionalProxyMiddlewareOptions( + "http", "localhost", proxyToPort.ToString(), requestTimeout); + appBuilder.UseMiddleware(publicPath, proxyOptions); } #pragma warning disable CS0649 diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts index 23db681..416aa89 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts @@ -19,6 +19,7 @@ export interface CreateDevServerCallback { interface CreateDevServerOptions { webpackConfigPath: string; suppliedOptions: DevServerOptions; + hotModuleReplacementEndpointUrl: string; } // These are the options configured in C# and then JSON-serialized, hence the C#-style naming @@ -28,7 +29,7 @@ interface DevServerOptions { ReactHotModuleReplacement: boolean; } -function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrEndpoint: string) { +function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configuration, enableHotModuleReplacement: boolean, enableReactHotModuleReplacement: boolean, hmrClientEndpoint: string, hmrServerEndpoint: string) { // Build the final Webpack config based on supplied options if (enableHotModuleReplacement) { // 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 // Augment all entry points so they support HMR (unless they already do) Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => { const webpackHotMiddlewareEntryPoint = 'webpack-hot-middleware/client'; - const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrEndpoint); + const webpackHotMiddlewareOptions = `?path=` + encodeURIComponent(hmrClientEndpoint); if (typeof entryPoints[entryPointName] === 'string') { entryPoints[entryPointName] = [webpackHotMiddlewareEntryPoint + webpackHotMiddlewareOptions, entryPoints[entryPointName]]; } else if (firstIndexOfStringStartingWith(entryPoints[entryPointName], webpackHotMiddlewareEntryPoint) < 0) { @@ -117,7 +118,9 @@ function attachWebpackDevMiddleware(app: any, webpackConfig: webpack.Configurati } catch (ex) { throw new Error('HotModuleReplacement failed because of an error while loading \'webpack-hot-middleware\'. Error was: ' + ex.stack); } - app.use(webpackHotMiddlewareModule(compiler)); + app.use(webpackHotMiddlewareModule(compiler, { + path: hmrServerEndpoint + })); } } @@ -198,8 +201,16 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option } normalizedPublicPaths.push(removeTrailingSlash(publicPath)); - const hmrEndpoint = `http://localhost:${listener.address().port}/__webpack_hmr`; - attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrEndpoint); + // Newer versions of Microsoft.AspNetCore.SpaServices will explicitly pass an HMR endpoint URL + // (because it's relative to the app's URL space root, which the client doesn't otherwise know). + // For back-compatibility, fall back on connecting directly to the underlying HMR server (though + // that won't work if the app is hosted on HTTPS because of the mixed-content rule, and we can't + // run the HMR server itself on HTTPS because in general it has no valid cert). + const hmrClientEndpoint = options.hotModuleReplacementEndpointUrl // The URL that we'll proxy (e.g., /__asp_webpack_hmr) + || `http://localhost:${listener.address().port}/__webpack_hmr`; // Fall back on absolute URL to bypass proxying + const hmrServerEndpoint = options.hotModuleReplacementEndpointUrl + || '/__webpack_hmr'; // URL is relative to webpack dev server root + attachWebpackDevMiddleware(app, webpackConfig, enableHotModuleReplacement, enableReactHotModuleReplacement, hmrClientEndpoint, hmrServerEndpoint); } });