[英]How to intercept 404 using Owin middleware
First let me explain the background. 首先让我解释一下背景。 I am working on a project that attempts to marry a backend server that uses Web API configured via OWIN- hosted on IIS now, but potentially other OWIN-supported hosts in the future- to a frontend using AngularJS.
我正在研究一个项目,该项目试图将使用通过IIS上托管的OWIN配置的Web API的后端服务器结合起来,但未来可能还有其他OWIN支持的主机 - 使用AngularJS的前端。
The AngularJS frontend is entirely static content. AngularJS前端完全是静态内容。 I completely avoid server-side technologies such as MVC/Razor, WebForms, Bundles, anything that has to do with the frontend and the assets it uses, and defer instead to the latest and greatest techniques using Node.js, Grunt/Gulp, etc. to handle CSS compilation, bundling, minification, etc. For reasons I won't go into here, I keep the frontend and server projects in separate locations within the same project (rather than stick them all in the Host project directly (see crude diagram below).
我完全避免服务器端技术,如MVC / Razor,WebForms,Bundles,任何与前端及其使用的资产有关的技术,而是推迟使用Node.js,Grunt / Gulp等最新最好的技术。处理CSS编译,捆绑,缩小等等。由于我不会进入这里的原因,我将前端和服务器项目保存在同一个项目中的不同位置(而不是直接将它们全部放在Host项目中(参见raw下图)。
MyProject.sln
server
MyProject.Host
MyProject.Host.csproj
Startup.cs
(etc.)
frontend
MyProjectApp
app.js
index.html
MyProjectApp.njproj
(etc.)
So as far as the frontend is concerned, all I need to do is get my Host to serve my static content. 因此就前端而言,我需要做的就是让我的主机服务我的静态内容。 In Express.js, this is trivial.
在Express.js中,这是微不足道的。 With OWIN, I was able to do this easily using Microsoft.Owin.StaticFiles middleware, and it works great (it's very slick).
使用OWIN,我能够使用Microsoft.Owin.StaticFiles中间件轻松完成这项工作,并且它运行良好(非常灵活)。
Here is my OwinStartup
configuration: 这是我的
OwinStartup
配置:
string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path
string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory
app.UseFileServer(new FileServerOptions
{
EnableDefaultFiles = true,
FileSystem = new PhysicalFileSystem(contentPath),
RequestPath = new PathString(string.Empty) // starts at the root of the host
});
// ensure the above occur before map handler to prevent native static content handler
app.UseStageMarker(PipelineStage.MapHandler);
Basically, it just hosts everything in frontend/MyProjectApp
as if it were right inside the root of MyProject.Host. 基本上,它只是在
frontend/MyProjectApp
托管所有内容,就好像它位于MyProject.Host的根目录中一样。 So naturally, if you request a file that doesn't exist, IIS generates a 404 error. 很自然地,如果您请求不存在的文件,IIS会生成404错误。
Now, because this is an AngularJS app, and it supports html5mode
, I will have some routes that aren't physical files on the server, but are handled as routes in the AngularJS app. 现在,因为这是一个AngularJS应用程序,它支持
html5mode
,我将在服务器上有一些不是物理文件的路由,但是在AngularJS应用程序中作为路由处理。 If a user were to drop onto an AngularJS (anything other than index.html
or a file that physically exists, in this example), I would get a 404 even though that route might be valid in the AngularJS app. 如果用户要放入AngularJS(除了
index.html
之外的任何东西或者实际存在的文件,在本例中),即使该路由在AngularJS应用程序中有效,我也会得到404。 Therefore, I need my OWIN middleware to return the index.html
file in the event a requested file does not exist, and let my AngularJS app figure out if it really is a 404. 因此,我需要我的OWIN中间件在所请求的文件不存在的情况下返回
index.html
文件,让我的AngularJS应用程序确定它是否真的是404。
If you're familiar with SPAs and AngularJS, this is a normal and straight-forward approach. 如果您熟悉SPA和AngularJS,这是一种正常而直接的方法。 If I were using MVC or ASP.NET routing, I could just set the default route to an MVC controller that returns my
index.html
, or something along those lines. 如果我使用的是MVC或ASP.NET路由,我可以将默认路由设置为返回我的
index.html
的MVC控制器,或者沿着这些行。 However, I've already stated I'm not using MVC and I'm trying to keep this as simple and lightweight as possible. 但是,我已经说过我没有使用MVC,我试图尽量保持简单和轻量级。
This user had a similar dilemma and solved it with IIS rewriting. 这个用户有类似的困境,并通过IIS重写解决了它。 In my case, it doesn't work because a) my content doesn't physically exist where the rewrite URL module can find it, so it always returns
index.html
and b) I want something that doesn't rely on IIS, but is handled within OWIN middleware so it can be used flexibly. 在我的情况下,它不起作用,因为a)我的内容实际上不存在重写URL模块可以找到它,因此它总是返回
index.html
和b)我想要的东西不依赖于IIS,但在OWIN中间件中处理,因此可以灵活使用。
Simple, how can I intercept a 404 Not Found and return the content of (note: not redirect) my FileServer
-served index.html
using OWIN middleware? 很简单,如何拦截404 Not Found并使用OWIN中间件返回我的
FileServer
-served index.html
的内容(注意: 不重定向)?
If you're using OWIN, you should be able to use this: 如果你正在使用OWIN,你应该能够使用它:
using AppFunc = Func<
IDictionary<string, object>, // Environment
Task>; // Done
public static class AngularServerExtension
{
public static IAppBuilder UseAngularServer(this IAppBuilder builder, string rootPath, string entryPath)
{
var options = new AngularServerOptions()
{
FileServerOptions = new FileServerOptions()
{
EnableDirectoryBrowsing = false,
FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
},
EntryPath = new PathString(entryPath)
};
builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);
return builder.Use(new Func<AppFunc, AppFunc>(next => new AngularServerMiddleware(next, options).Invoke));
}
}
public class AngularServerOptions
{
public FileServerOptions FileServerOptions { get; set; }
public PathString EntryPath { get; set; }
public bool Html5Mode
{
get
{
return EntryPath.HasValue;
}
}
public AngularServerOptions()
{
FileServerOptions = new FileServerOptions();
EntryPath = PathString.Empty;
}
}
public class AngularServerMiddleware
{
private readonly AngularServerOptions _options;
private readonly AppFunc _next;
private readonly StaticFileMiddleware _innerMiddleware;
public AngularServerMiddleware(AppFunc next, AngularServerOptions options)
{
_next = next;
_options = options;
_innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
}
public async Task Invoke(IDictionary<string, object> arg)
{
await _innerMiddleware.Invoke(arg);
// route to root path if the status code is 404
// and need support angular html5mode
if ((int)arg["owin.ResponseStatusCode"] == 404 && _options.Html5Mode)
{
arg["owin.RequestPath"] = _options.EntryPath.Value;
await _innerMiddleware.Invoke(arg);
}
}
}
The solution that Javier Figueroa provided really works for my project. Javier Figueroa提供的解决方案真正适用于我的项目。 The back end of my program is an OWIN self-hosted webserver, and I use AngularJS with html5Mode enabled as the front end.
我的程序的后端是一个OWIN自托管网络服务器,我使用AngularJS并启用了html5Mode作为前端。 I tried many different ways writing a IOwinContext middleware and none of them works till I found this one, it finally works!
我尝试了很多不同的方法来编写IOwinContext中间件,但在找到这个中间件之前它们都没有工作,它终于有效了! Thanks for sharing this solution.
感谢您分享此解决方案。
solution provided by Javier Figueroa 解决方案由Javier Figueroa提供
By the way, the following is how I apply that AngularServerExtension in my OWIN startup class: 顺便说一句,以下是我在我的OWIN启动类中应用AngularServerExtension的方法:
// declare the use of UseAngularServer extention
// "/" <= the rootPath
// "/index.html" <= the entryPath
app.UseAngularServer("/", "/index.html");
// Setting OWIN based web root directory
app.UseFileServer(new FileServerOptions()
{
RequestPath = PathString.Empty,
FileSystem = new PhysicalFileSystem(@staticFilesDir), // point to the root directory of my web server
});
I wrote this little middleware component, but I don't know if it's overkill, inefficient, or if there are other pitfalls. 我写了这个小中间件组件,但我不知道它是否过度,低效,或者是否存在其他陷阱。 Basically it just takes in the same
FileServerOptions
the FileServerMiddleware
uses, the most important part being the FileSystem
we're using. 基本上它只需要
FileServerMiddleware
使用的相同FileServerOptions
,最重要的部分是我们正在使用的FileSystem
。 It is placed before the aforementioned middleware and does a quick check to see if the requested path exists. 它位于上述中间件之前,并快速检查所请求的路径是否存在。 If not, the request path is rewritten as "index.html", and the normal StaticFileMiddleware will take over from there.
如果没有,请求路径将被重写为“index.html”,正常的StaticFileMiddleware将从那里接管。
Obviously it could stand to be cleaned up for reuse, including a way to define different default files for different root paths (eg anything requested from "/feature1" that is missing should use "/feature1/index.html", likewise with "/feature2" and "/feature2/default.html", etc.). 显然它可以被清理以便重复使用,包括为不同的根路径定义不同的默认文件的方法(例如,从“/ feature1”请求的任何东西都应该使用“/feature1/index.html”,同样使用“/ feature2“和”/feature2/default.html“等)。
But for now, it this works for me. 但就目前而言,这对我有用。 This has a dependency on Microsoft.Owin.StaticFiles, obviously.
显然,这依赖于Microsoft.Owin.StaticFiles。
public class DefaultFileRewriterMiddleware : OwinMiddleware
{
private readonly FileServerOptions _options;
/// <summary>
/// Instantiates the middleware with an optional pointer to the next component.
/// </summary>
/// <param name="next"/>
/// <param name="options"></param>
public DefaultFileRewriterMiddleware(OwinMiddleware next, FileServerOptions options) : base(next)
{
_options = options;
}
#region Overrides of OwinMiddleware
/// <summary>
/// Process an individual request.
/// </summary>
/// <param name="context"/>
/// <returns/>
public override async Task Invoke(IOwinContext context)
{
IFileInfo fileInfo;
PathString subpath;
if (!TryMatchPath(context, _options.RequestPath, false, out subpath) ||
!_options.FileSystem.TryGetFileInfo(subpath.Value, out fileInfo))
{
context.Request.Path = new PathString(_options.RequestPath + "/index.html");
}
await Next.Invoke(context);
}
#endregion
internal static bool PathEndsInSlash(PathString path)
{
return path.Value.EndsWith("/", StringComparison.Ordinal);
}
internal static bool TryMatchPath(IOwinContext context, PathString matchUrl, bool forDirectory, out PathString subpath)
{
var path = context.Request.Path;
if (forDirectory && !PathEndsInSlash(path))
{
path += new PathString("/");
}
if (path.StartsWithSegments(matchUrl, out subpath))
{
return true;
}
return false;
}
}
Answer given by Javier Figueroa here works and really helpful! Javier Figueroa给出的答案在这里起作用并且非常有用! Thanks for that!
感谢那! However, it has one odd behavior: whenever nothing exists (including the entry file), it runs
next
pipeline twice. 但是,它有一个奇怪的行为:只要不存在(包括条目文件),它就会运行
next
管道两次。 For example, below test fails when I apply that implementation through UseHtml5Mode
: 例如,当我通过
UseHtml5Mode
应用该实现时,下面的测试失败:
[Test]
public async Task ShouldRunNextMiddlewareOnceWhenNothingExists()
{
// ARRANGE
int hitCount = 0;
var server = TestServer.Create(app =>
{
app.UseHtml5Mode("test-resources", "/does-not-exist.html");
app.UseCountingMiddleware(() => { hitCount++; });
});
using (server)
{
// ACT
await server.HttpClient.GetAsync("/does-not-exist.html");
// ASSERT
Assert.AreEqual(1, hitCount);
}
}
A few notes about my above test if anyone is intrested:
关于我的上述测试的一些注释,如果有人有兴趣:
- It uses Microsoft.Owin.Testing .
它使用Microsoft.Owin.Testing 。
- Test framework is NUnit .
测试框架是NUnit 。
UseCountingMiddleware
implementation is available here .UseCountingMiddleware
实现可在此处获得 。
The implementation I went with which makes the above test pass is as below: 我使用的实现使上述测试通过如下:
namespace Foo
{
using AppFunc = Func<IDictionary<string, object>, Task>;
public class Html5ModeMiddleware
{
private readonly Html5ModeOptions m_Options;
private readonly StaticFileMiddleware m_InnerMiddleware;
private readonly StaticFileMiddleware m_EntryPointAwareInnerMiddleware;
public Html5ModeMiddleware(AppFunc next, Html5ModeOptions options)
{
if (next == null) throw new ArgumentNullException(nameof(next));
if (options == null) throw new ArgumentNullException(nameof(options));
m_Options = options;
m_InnerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
m_EntryPointAwareInnerMiddleware = new StaticFileMiddleware((environment) =>
{
var context = new OwinContext(environment);
context.Request.Path = m_Options.EntryPath;
return m_InnerMiddleware.Invoke(environment);
}, options.FileServerOptions.StaticFileOptions);
}
public Task Invoke(IDictionary<string, object> environment) =>
m_EntryPointAwareInnerMiddleware.Invoke(environment);
}
}
The extension is pretty similar: 扩展非常相似:
namespace Owin
{
using AppFunc = Func<IDictionary<string, object>, Task>;
public static class AppBuilderExtensions
{
public static IAppBuilder UseHtml5Mode(this IAppBuilder app, string rootPath, string entryPath)
{
if (app == null) throw new ArgumentNullException(nameof(app));
if (rootPath == null) throw new ArgumentNullException(nameof(rootPath));
if (entryPath == null) throw new ArgumentNullException(nameof(entryPath));
var options = new Html5ModeOptions
{
EntryPath = new PathString(entryPath),
FileServerOptions = new FileServerOptions()
{
EnableDirectoryBrowsing = false,
FileSystem = new PhysicalFileSystem(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
}
};
app.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);
return app.Use(new Func<AppFunc, AppFunc>(next => new Html5ModeMiddleware(next, options).Invoke));
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.