【问题标题】:How to intercept 404 using Owin middleware如何使用 Owin 中间件拦截 404
【发布时间】:2015-01-18 03:04:53
【问题描述】:

背景

首先让我解释一下背景。我正在开发一个项目,该项目试图将使用通过 OWIN 配置的 Web API 配置的后端服务器(现在托管在 IIS 上,但将来可能会使用其他 OWIN 支持的主机)与使用 AngularJS 的前端结合。

AngularJS 前端完全是静态内容。我完全避免使用 MVC/Razor、WebForms、Bundles 等服务器端技术,以及与前端及其使用的资产有关的任何东西,而是使用 Node.js、Grunt/Gulp 等最新最好的技术. 处理 CSS 编译、捆绑、缩小等。由于我不会在这里讨论的原因,我将前端和服务器项目放在同一个项目中的不同位置(而不是直接将它们全部放在 Host 项目中(见粗略)下图)。

MyProject.sln
server
  MyProject.Host
     MyProject.Host.csproj
     Startup.cs
     (etc.)
frontend
  MyProjectApp
     app.js
     index.html
     MyProjectApp.njproj
     (etc.)

所以就前端而言,我需要做的就是让我的主机为我的静态内容提供服务。在 Express.js 中,这是微不足道的。使用 OWIN,我可以使用 Microsoft.Owin.StaticFiles 中间件轻松完成此操作,而且效果很好(非常流畅)。

这是我的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);

渔获

基本上,它只是在frontend/MyProjectApp 中托管所有内容,就好像它就在 MyProject.Host 的根目录中一样。所以很自然,如果你请求一个不存在的文件,IIS 会产生一个 404 错误。

现在,因为这是一个 AngularJS 应用程序,并且它支持 html5mode,所以我将有一些不是服务器上的物理文件的路由,而是在 AngularJS 应用程序中作为路由处理。如果用户要放到 AngularJS 上(在此示例中,除了 index.html 或物理存在的文件之外的任何内容),即使该路由在 AngularJS 应用程序中可能有效,我也会得到 404。因此,如果请求的文件不存在,我需要我的 OWIN 中间件返回 index.html 文件,并让我的 AngularJS 应用程序确定它是否真的是 404。

如果您熟悉 SPA 和 AngularJS,这是一种正常且直接的方法。如果我使用 MVC 或 ASP.NET 路由,我可以将默认路由设置为返回我的 index.html 的 MVC 控制器,或者类似的东西。但是,我已经声明我没有使用 MVC,并且我正在尝试使其尽可能简单和轻量级。

This user 有类似的困境,并通过 IIS 重写解决了它。在我的情况下,它不起作用,因为 a) 我的内容实际上并不存在于重写 URL 模块可以找到它的地方,所以它总是 返回 index.html 和 b) 我想要一些不'不依赖IIS,而是在OWIN中间件中处理,所以可以灵活使用。

TL;DNR 我,大声哭泣。

很简单,如何使用 OWIN 中间件拦截 404 Not Found 并返回(注意:not 重定向)我的 FileServer-served index.html 的内容?

【问题讨论】:

    标签: c# asp.net owin static-files owin-middleware


    【解决方案1】:

    如果你使用的是 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);
            }
        }
    }
    

    【讨论】:

    • AngularServerMiddleware 的最后一行应该是await _next.Invoke(arg)。否则_next不被使用,我相信链会被打破。
    • @Swoogan 不,因为StaticFileMiddleware 在收到 404 时会调用 next
    • 与@Swoogan 上面所说的有关,但它有一个奇怪的行为。请参阅stackoverflow.com/a/38898208/463785 以获得更好的解决方案,该解决方案主要基于此解决方案。
    【解决方案2】:

    Javier Figueroa 提供的解决方案非常适合我的项目。我的程序的后端是一个 OWIN 自托管网络服务器,我使用启用了 html5Mode 的 AngularJS 作为前端。我尝试了许多不同的方法来编写 IOwinContext 中间件,但在我找到这个中间件之前它们都不起作用,它终于起作用了!感谢您分享此解决方案。

    solution provided by Javier Figueroa

    顺便说一下,以下是我在 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
            });
    

    【讨论】:

      【解决方案3】:

      我写了这个中间件小组件,但我不知道它是否矫枉过正、效率低下,还是有其他陷阱。基本上,它只采用与FileServerMiddleware 相同的FileServerOptions,最重要的部分是我们正在使用的FileSystem。它放置在上述中间件之前,并快速检查请求的路径是否存在。如果不是,则请求路径被重写为“index.html”,并且普通的 StaticFileMiddleware 将从那里接管。

      显然它可以被清理以供重用,包括为不同的根路径定义不同的默认文件的方法(例如,从“/feature1”请求的任何缺少的东西都应该使用“/feature1/index.html”,同样与“/feature2”和“/feature2/default.html”等)。

      但是现在,它对我有用。这显然依赖于 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;
          }
      }
      

      【讨论】:

        【解决方案4】:

        Javier Figueroahere 给出的答案很有效,真的很有帮助!感谢那!但是,它有一个奇怪的行为:只要不存在任何内容(包括入口文件),它就会运行next 管道两次。例如,当我通过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);
            }
        }
        

        如果有人感兴趣的话,关于我的上述测试的几点说明:

        使上述测试通过的实现如下:

        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);
            }
        }
        

        扩展非常相似:

        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));
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-22
          • 2019-06-11
          • 1970-01-01
          • 2019-03-15
          • 2011-09-26
          • 1970-01-01
          相关资源
          最近更新 更多