Как перехватить 404 с помощью промежуточного программного обеспечения Owin

Фон

Сначала позвольте мне объяснить предысторию. Я работаю над проектом, который пытается объединить внутренний сервер, который использует веб-API, настроенный через OWIN, размещенный сейчас на IIS, но потенциально другие хосты с поддержкой OWIN в будущем, с интерфейсом, использующим AngularJS.

Интерфейс AngularJS - это полностью статический контент. Я полностью избегаю серверных технологий, таких как MVC / Razor, WebForms, Bundles, всего, что связано с интерфейсом и используемыми им активами, и вместо этого использую новейшие и лучшие методы с использованием Node.js, Grunt / Gulp и т. Д. . для обработки компиляции CSS, объединения, минификации и т. д. По причинам, которые я не буду здесь вдаваться, я храню проекты внешнего интерфейса и сервера в разных местах в одном проекте (вместо того, чтобы вставлять их все напрямую в проект хоста (см. диаграмму ниже).

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 или файла, который физически существует, в этом примере), я бы получил 404, даже если этот маршрут мог бы быть действительным в приложении AngularJS. Поэтому мне нужно, чтобы мое промежуточное ПО OWIN возвращало файл index.html в случае, если запрошенный файл не существует, и позволяло моему приложению AngularJS выяснить, действительно ли это 404.

Если вы знакомы с SPA и AngularJS, это нормальный и простой подход. Если бы я использовал маршрутизацию MVC или ASP.NET, я мог бы просто установить маршрут по умолчанию к контроллеру MVC, который возвращает мой index.html или что-то в этом роде. Однако я уже заявлял, что не использую MVC, и я стараюсь сделать это как можно более простым и легким.

У этого пользователя была аналогичная дилемма, и он решил ее с помощью перезаписи IIS. В моем случае это не работает, потому что а) мой контент физически не существует там, где модуль перезаписи URL может его найти, поэтому он всегда возвращает index.html и б) я хочу что-то, что не полагаются на IIS, но обрабатываются в промежуточном программном обеспечении OWIN, поэтому его можно использовать гибко.

TL; DNR меня, за то, что громко кричал.

Просто, как я могу перехватить 404 Not Found и вернуть содержимое (примечание: не перенаправления) моего FileServer обслуживаемого index.html с помощью промежуточного программного обеспечения OWIN?


person moribvndvs    schedule 20.11.2014    source источник


Ответы (4)


Если вы используете 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);
        }
    }
}
person Javier Figueroa    schedule 09.06.2015
comment
Если последняя строка AngularServerMiddleware будет await _next.Invoke(arg). В противном случае _next не будет использоваться, и я считаю, что цепочка будет разорвана. - person Swoogan; 24.03.2016
comment
@Swoogan нет, это не нужно, поскольку StaticFileMiddleware вызывает next всякий раз, когда получает 404. - person tugberk; 11.08.2016
comment
Однако в связи с тем, что сказал @Swoogan выше, у него есть одно странное поведение. См. stackoverflow.com/a/38898208/463785, чтобы получить немного лучшее решение, которое в основном основано на этом. - person tugberk; 11.08.2016

Решение, которое предоставил Хавьер Фигероа, действительно работает для моего проекта. Внутренняя часть моей программы - это автономный веб-сервер OWIN, и я использую AngularJS с включенным html5Mode в качестве внешнего интерфейса. Я пробовал много разных способов написания промежуточного программного обеспечения IOwinContext, и ни один из них не работал, пока я не нашел этот, он, наконец, работает! Спасибо, что поделились этим решением.

решение, предоставленное Хавьером Фигероа

Кстати, вот как я применяю это AngularServerExtension в моем стартовом классе OWIN:

        // 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
        });
person Tom Liao    schedule 24.08.2015

Я написал этот небольшой компонент промежуточного программного обеспечения, но я не знаю, является ли он излишним, неэффективным или есть другие подводные камни. По сути, он просто принимает то же FileServerOptions, что используется FileServerMiddleware, наиболее важной частью является 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;
    }
}
person moribvndvs    schedule 20.11.2014

Ответ дал Хавьер Фигероа вот работает и действительно полезно! Спасибо за это! Однако у него есть одно странное поведение: всякий раз, когда ничего не существует (включая файл записи), он запускает конвейер 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));
        }
    }
}
person tugberk    schedule 11.08.2016