My current job, the plugin to serve the default page for all "not found" routes without changing the URL in the browser, includes most of what you need to handle the global wildcard pattern. Use it to get started.
To understand what this code does, it helps to understand the priority of ServiceStack routing and how CatchAllHandlers fit into this process. ServiceStack calls ServiceStackHttpHandlerFactory.GetHandler to get a handler for the current route.
ServiceStackHttpHandlerFactory.GetHandler returns:
- Relevant RawHttpHandler, if any.
- If the domain is root, the handler returns
GetCatchAllHandlerIfAny(...), , if any. - If the route matches the metadata uri (I will skip the exact logic here, since this is not important for your question), the appropriate handler, if any.
- The handler returns
ServiceStackHttpHandlerFactory.GetHandlerForPathInfo , if any. - NotFoundHandler.
ServiceStackHttpHandlerFactory.GetHandlerForPathInfo returns:
- If the URL matches a valid REST route, a new RestHandler.
- If the URL matches an existing file or directory, it returns
- the handler returned by
GetCatchAllHandlerIfAny(...), , if any. - If it is a supported file type, StaticFileHandler,
- If this is not a supported file type, ForbiddenHttpHandler.
- The handler returns
GetCatchAllHandlerIfAny(...), , if any. - zero.
The CatchAllHandlers array contains functions that evaluate the URL and return a handler, or null. The functions in the array are called sequentially, and the first, which does not return null, processes the route. Let me highlight some key elements:
First, the plugin adds CatchAllHandler to the appHost.CatchAllHandlers array when registering.
public void Register(IAppHost appHost) { appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) => Factory(method, pathInfo, filepath)); }
Secondly, CatchAllHandler. As described above, this function can be called for the root of a domain, an existing file or directory, or any other unsurpassed route. Your method should return a handler if your criteria is met, or returns null.
private static Html5ModeFeature Factory(String method, String pathInfo, String filepath) { var Html5ModeHandler = Html5ModeFeature.Instance; List<string> WebHostRootFileNames = RootFiles(); // handle domain root if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/") { return Html5ModeHandler; } // don't handle 'mode' urls var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath; if (mode != null && pathInfo.EndsWith(mode)) { return null; } var pathParts = pathInfo.TrimStart('/').Split('/'); var existingFile = pathParts[0].ToLower(); var catchAllHandler = new Object(); if (WebHostRootFileNames.Contains(existingFile)) { var fileExt = Path.GetExtension(filepath); var isFileRequest = !string.IsNullOrEmpty(fileExt); // don't handle directories or files that have another handler catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath); if (catchAllHandler != null) return null; // don't handle existing files under any event return isFileRequest ? null : Html5ModeHandler; } // don't handle non-physical urls that have another handler catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath); if (catchAllHandler != null) return null; // handle anything else return Html5ModeHandler; }
In the case of a wildcard in the root domain, you may not want to capture routes that can be handled by another CatchAllHandler. If so, to avoid infinite recursion, you will need your own GetCatchAllHandlerIfAny method.
// // local copy of ServiceStackHttpHandlerFactory.GetCatchAllHandlerIfAny, prevents infinite recursion // private static IHttpHandler GetCatchAllHandlerIfAny(string httpMethod, string pathInfo, string filePath) { if (EndpointHost.CatchAllHandlers != null) { foreach (var httpHandlerResolver in EndpointHost.CatchAllHandlers) { if (httpHandlerResolver == Html5ModeFeature.Factory) continue; // avoid infinite recursion var httpHandler = httpHandlerResolver(httpMethod, pathInfo, filePath); if (httpHandler != null) return httpHandler; } } return null; }
Here is a complete and completely untested plugin. It compiles. He makes no warranties of suitability for any particular purpose.
using ServiceStack; using ServiceStack.Common.Web; using ServiceStack.Razor; using ServiceStack.ServiceHost; using ServiceStack.Text; using ServiceStack.WebHost.Endpoints; using ServiceStack.WebHost.Endpoints.Formats; using ServiceStack.WebHost.Endpoints.Support; using ServiceStack.WebHost.Endpoints.Support.Markdown; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Web; namespace MyProject.Support { public enum DefaultFileFormat { Markdown, Razor, Static } public class Html5ModeFeature : EndpointHandlerBase, IPlugin { private FileInfo fi { get; set; } private DefaultFileFormat FileFormat { get; set; } private DateTime FileModified { get; set; } private byte[] FileContents { get; set; } public MarkdownHandler Markdown { get; set; } public RazorHandler Razor { get; set; } public object Model { get; set; } private static Dictionary<string, string> allDirs; public string PathInfo { get; set; } public void Register(IAppHost appHost) { appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) => Factory(method, pathInfo, filepath)); } private Html5ModeFeature() { foreach (var defaultDoc in EndpointHost.Config.DefaultDocuments) { if (PathInfo == null) { var defaultFileName = Path.Combine(Directory.GetCurrentDirectory(), defaultDoc); if (!File.Exists(defaultFileName)) continue; PathInfo = (String)defaultDoc; // use first default document found. } } SetFile(); } private static Html5ModeFeature instance; public static Html5ModeFeature Instance { get { return instance ?? (instance = new Html5ModeFeature()); } } public void SetFile() { if (PathInfo.EndsWith(MarkdownFormat.MarkdownExt) || PathInfo.EndsWith(MarkdownFormat.TemplateExt)) { Markdown = new MarkdownHandler(PathInfo); FileFormat = DefaultFileFormat.Markdown; return; } if (PathInfo.EndsWith(Razor.RazorFormat.RazorFileExtension)) { Razor = new RazorHandler(PathInfo); FileFormat = DefaultFileFormat.Razor; return; } FileContents = File.ReadAllBytes(PathInfo); FileModified = File.GetLastWriteTime(PathInfo); FileFormat = DefaultFileFormat.Static; } // // ignore request.PathInfo, return default page, extracted from StaticFileHandler.ProcessResponse // public void ProcessStaticPage(IHttpRequest request, IHttpResponse response, string operationName) { response.EndHttpHandlerRequest(skipClose: true, afterBody: r => { TimeSpan maxAge; if (r.ContentType != null && EndpointHost.Config.AddMaxAgeForStaticMimeTypes.TryGetValue(r.ContentType, out maxAge)) { r.AddHeader(HttpHeaders.CacheControl, "max-age=" + maxAge.TotalSeconds); } if (request.HasNotModifiedSince(fi.LastWriteTime)) { r.ContentType = MimeTypes.GetMimeType(PathInfo); r.StatusCode = 304; return; } try { r.AddHeaderLastModified(fi.LastWriteTime); r.ContentType = MimeTypes.GetMimeType(PathInfo); if (fi.LastWriteTime > this.FileModified) SetFile(); //reload r.OutputStream.Write(this.FileContents, 0, this.FileContents.Length); r.Close(); return; } catch (Exception ex) { throw new HttpException(403, "Forbidden."); } }); } private void ProcessServerError(IHttpRequest httpReq, IHttpResponse httpRes, string operationName) { var sb = new StringBuilder(); sb.AppendLine("{"); sb.AppendLine("\"ResponseStatus\":{"); sb.AppendFormat(" \"ErrorCode\":{0},\n", 500); sb.AppendFormat(" \"Message\": HTML5ModeHandler could not serve file {0}.\n", PathInfo.EncodeJson()); sb.AppendLine("}"); sb.AppendLine("}"); httpRes.EndHttpHandlerRequest(skipClose: true, afterBody: r => { r.StatusCode = 500; r.ContentType = ContentType.Json; var sbBytes = sb.ToString().ToUtf8Bytes(); r.OutputStream.Write(sbBytes, 0, sbBytes.Length); r.Close(); }); return; } private static List<string> RootFiles() { var WebHostPhysicalPath = EndpointHost.Config.WebHostPhysicalPath; List<string> WebHostRootFileNames = new List<string>(); foreach (var filePath in Directory.GetFiles(WebHostPhysicalPath)) { var fileNameLower = Path.GetFileName(filePath).ToLower(); WebHostRootFileNames.Add(Path.GetFileName(fileNameLower)); } foreach (var dirName in Directory.GetDirectories(WebHostPhysicalPath)) { var dirNameLower = Path.GetFileName(dirName).ToLower(); WebHostRootFileNames.Add(Path.GetFileName(dirNameLower)); } return WebHostRootFileNames; } private static Html5ModeFeature Factory(String method, String pathInfo, String filepath) { var Html5ModeHandler = Html5ModeFeature.Instance; List<string> WebHostRootFileNames = RootFiles(); // handle domain root if (string.IsNullOrEmpty(pathInfo) || pathInfo == "/") { return Html5ModeHandler; } // don't handle 'mode' urls var mode = EndpointHost.Config.ServiceStackHandlerFactoryPath; if (mode != null && pathInfo.EndsWith(mode)) { return null; } var pathParts = pathInfo.TrimStart('/').Split('/'); var existingFile = pathParts[0].ToLower(); var catchAllHandler = new Object(); if (WebHostRootFileNames.Contains(existingFile)) { var fileExt = Path.GetExtension(filepath); var isFileRequest = !string.IsNullOrEmpty(fileExt); // don't handle directories or files that have another handler catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath); if (catchAllHandler != null) return null; // don't handle existing files under any event return isFileRequest ? null : Html5ModeHandler; } // don't handle non-physical urls that have another handler catchAllHandler = GetCatchAllHandlerIfAny(method, pathInfo, filepath); if (catchAllHandler != null) return null; // handle anything else return Html5ModeHandler; } // // Local copy of private StaticFileHandler.DirectoryExists // public static bool DirectoryExists(string dirPath, string appFilePath) { if (dirPath == null) return false; try { if (!ServiceStack.Text.Env.IsMono) return Directory.Exists(dirPath); } catch { return false; } if (allDirs == null) allDirs = CreateDirIndex(appFilePath); var foundDir = allDirs.ContainsKey(dirPath.ToLower()); //log.DebugFormat("Found dirPath {0} in Mono: ", dirPath, foundDir); return foundDir; } // // Local copy of private StaticFileHandler.CreateDirIndex // static Dictionary<string, string> CreateDirIndex(string appFilePath) { var indexDirs = new Dictionary<string, string>(); foreach (var dir in GetDirs(appFilePath)) { indexDirs[dir.ToLower()] = dir; } return indexDirs; } // // Local copy of private StaticFileHandler.GetDirs // static List<string> GetDirs(string path) { var queue = new Queue<string>(); queue.Enqueue(path); var results = new List<string>(); while (queue.Count > 0) { path = queue.Dequeue(); try { foreach (string subDir in Directory.GetDirectories(path)) { queue.Enqueue(subDir); results.Add(subDir); } } catch (Exception ex) { Console.Error.WriteLine(ex); } } return results; } // // local copy of ServiceStackHttpHandlerFactory.GetCatchAllHandlerIfAny, prevents infinite recursion // private static IHttpHandler GetCatchAllHandlerIfAny(string httpMethod, string pathInfo, string filePath) { if (EndpointHost.CatchAllHandlers != null) { foreach (var httpHandlerResolver in EndpointHost.CatchAllHandlers) { if (httpHandlerResolver == Html5ModeFeature.Factory) continue; // avoid infinite recursion var httpHandler = httpHandlerResolver(httpMethod, pathInfo, filePath); if (httpHandler != null) return httpHandler; } } return null; } public override void ProcessRequest(IHttpRequest httpReq, IHttpResponse httpRes, string operationName) { switch (FileFormat) { case DefaultFileFormat.Markdown: { Markdown.ProcessRequest(httpReq, httpRes, operationName); break; } case DefaultFileFormat.Razor: { Razor.ProcessRequest(httpReq, httpRes, operationName); break; } case DefaultFileFormat.Static: { fi.Refresh(); if (fi.Exists) ProcessStaticPage(httpReq, httpRes, operationName); else ProcessServerError(httpReq, httpRes, operationName); break; } default: { ProcessServerError(httpReq, httpRes, operationName); break; } } } public override object CreateRequest(IHttpRequest request, string operationName) { return null; } public override object GetResponse(IHttpRequest httpReq, IHttpResponse httpRes, object request) { return null; } } }