Как перехватить 404 с помощью промежуточного ПО Owin
Фон
Сначала позвольте мне объяснить фон. Я работаю над проектом, который пытается объединить внутренний сервер, использующий Web API, настроенный через OWIN, размещенный на 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 я смог легко это сделать с помощью промежуточного программного обеспечения https://www.nuget.org/packages/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. В моем случае это не работает, потому что a) мой контент не существует физически, где модуль перезаписи URL может его найти, поэтому он всегда возвращает index.html
и б) я хочу что-то, что не зависит от IIS, но обрабатывается в промежуточном программном обеспечении OWIN, поэтому его можно использовать гибко.
TL; DNR меня, за то, что я громко плачу.
Просто, как я могу перехватить 404 Not Found и вернуть содержимое (примечание: не перенаправить) моего FileServer
-served index.html
используя промежуточное ПО OWIN?
4 ответа
Я написал этот небольшой компонент промежуточного программного обеспечения, но я не знаю, является ли он излишним, неэффективным или есть другие подводные камни. В основном это просто берет то же самое 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;
}
}
Если вы используете 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);
}
}
}
Решение, предоставленное Хавьером Фигероа, действительно работает для моего проекта. Back-end моей программы - это веб-сервер 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
});
Ответ, данный Javier Figueroa, здесь работает и действительно полезен! Спасибо за это! Тем не менее, он имеет одно странное поведение: когда ничего не существует (включая файл ввода), он запускается 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);
}
}
Несколько замечаний о моем тесте, если кто-то заинтересован:
- Он использует Microsoft.Owin.Testing.
- Тестовый фреймворк - это NUnit.
UseCountingMiddleware
реализация доступна здесь.
Реализация, с которой я пошел, которая делает вышеупомянутый тестовый проход, как показано ниже:
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));
}
}
}