diff --git a/src/Microsoft.AspNet.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNet.SpaServices/Webpack/ConditionalProxyMiddleware.cs new file mode 100644 index 0000000..95a693d --- /dev/null +++ b/src/Microsoft.AspNet.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -0,0 +1,92 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.SpaServices.Webpack +{ + // Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs + // Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain + // This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for + // chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). + internal class ConditionalProxyMiddleware { + private RequestDelegate next; + private ConditionalProxyMiddlewareOptions options; + private HttpClient httpClient; + private string pathPrefix; + + public ConditionalProxyMiddleware(RequestDelegate next, string pathPrefix, ConditionalProxyMiddlewareOptions options) + { + this.next = next; + this.pathPrefix = pathPrefix; + this.options = options; + this.httpClient = new HttpClient(new HttpClientHandler()); + } + + public async Task Invoke(HttpContext context) + { + if (context.Request.Path.StartsWithSegments(this.pathPrefix)) { + var didProxyRequest = await PerformProxyRequest(context); + if (didProxyRequest) { + return; + } + } + + // Not a request we can proxy + await this.next.Invoke(context); + } + + private async Task PerformProxyRequest(HttpContext context) { + var requestMessage = new HttpRequestMessage(); + + // Copy the request headers + foreach (var header in context.Request.Headers) { + if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null) { + requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + + requestMessage.Headers.Host = options.Host + ":" + options.Port; + var uriString = $"{options.Scheme}://{options.Host}:{options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; + requestMessage.RequestUri = new Uri(uriString); + requestMessage.Method = new HttpMethod(context.Request.Method); + + using (var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted)) { + if (responseMessage.StatusCode == HttpStatusCode.NotFound) { + // Let some other middleware handle this + return false; + } + + // We can handle this + context.Response.StatusCode = (int)responseMessage.StatusCode; + foreach (var header in responseMessage.Headers) { + context.Response.Headers[header.Key] = header.Value.ToArray(); + } + + foreach (var header in responseMessage.Content.Headers) { + context.Response.Headers[header.Key] = header.Value.ToArray(); + } + + // SendAsync removes chunking from the response. This removes the header so it doesn't expect a chunked response. + context.Response.Headers.Remove("transfer-encoding"); + await responseMessage.Content.CopyToAsync(context.Response.Body); + return true; + } + } + } + + internal class ConditionalProxyMiddlewareOptions { + public string Scheme { get; private set; } + public string Host { get; private set; } + public string Port { get; private set; } + + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) { + this.Scheme = scheme; + this.Host = host; + this.Port = port; + } + } +} diff --git a/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs index 1e2b4d5..8657c07 100644 --- a/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNet.NodeServices; -using Microsoft.AspNet.Proxy; using Microsoft.AspNet.SpaServices.Webpack; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; @@ -12,8 +11,10 @@ namespace Microsoft.AspNet.Builder { public static class WebpackDevMiddleware { + const string WebpackDevMiddlewareScheme = "http"; const string WebpackDevMiddlewareHostname = "localhost"; const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; + const string DefaultConfigFile = "webpack.config.js"; public static void UseWebpackDevMiddleware(this IApplicationBuilder appBuilder, WebpackDevMiddlewareOptions options = null) { // Validate options @@ -41,25 +42,21 @@ namespace Microsoft.AspNet.Builder // Tell Node to start the server hosting webpack-dev-middleware var devServerOptions = new { - webpackConfigPath = Path.Combine(appEnv.ApplicationBasePath, "webpack.config.js"), + webpackConfigPath = Path.Combine(appEnv.ApplicationBasePath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options ?? new WebpackDevMiddlewareOptions() }; var devServerInfo = nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener - appBuilder.Map(devServerInfo.PublicPath, builder => { - builder.RunProxy(new ProxyOptions { - Host = WebpackDevMiddlewareHostname, - Port = devServerInfo.Port.ToString() - }); - }); + var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, WebpackDevMiddlewareHostname, devServerInfo.Port.ToString()); + appBuilder.UseMiddleware(devServerInfo.PublicPath, proxyOptions); // While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream, // and the Microsoft.Aspnet.Proxy code doesn't handle that entirely - it throws an exception after // a while. So, just serve a 302 for those. appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => { builder.Use(next => async ctx => { - ctx.Response.Redirect($"http://localhost:{ devServerInfo.Port.ToString() }{ WebpackHotMiddlewareEndpoint }"); + ctx.Response.Redirect($"{ WebpackDevMiddlewareScheme }://{ WebpackDevMiddlewareHostname }:{ devServerInfo.Port.ToString() }{ WebpackHotMiddlewareEndpoint }"); await Task.Yield(); }); }); diff --git a/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs index a9510fc..b74f82d 100644 --- a/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs +++ b/src/Microsoft.AspNet.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs @@ -2,5 +2,6 @@ namespace Microsoft.AspNet.SpaServices.Webpack { public class WebpackDevMiddlewareOptions { public bool HotModuleReplacement { get; set; } public bool ReactHotModuleReplacement { get; set; } + public string ConfigFile { get; set; } } } diff --git a/src/Microsoft.AspNet.SpaServices/project.json b/src/Microsoft.AspNet.SpaServices/project.json index c54826e..542668f 100644 --- a/src/Microsoft.AspNet.SpaServices/project.json +++ b/src/Microsoft.AspNet.SpaServices/project.json @@ -15,7 +15,6 @@ "dependencies": { "Microsoft.AspNet.Mvc": "6.0.0-rc1-final", "Microsoft.AspNet.Routing": "1.0.0-rc1-final", - "Microsoft.AspNet.Proxy": "1.0.0-rc1-final", "Microsoft.AspNet.NodeServices": "1.0.0-alpha7" }, "frameworks": {