I am trying to write a REST web service using ServiceStack that accepts variable paths off of route. For example:
[Route(\"/{group}\"]
public class Entity
My current work in progress, a plugin to serve the default page to all 'not found' routes without changing the url in the browser, includes most of what you'll need to handle a global wildcard route. Use it to get you started.
To understand what this code is doing, it helps to understand ServiceStack's routing priority, and how CatchAllHandlers fit into the process. ServiceStack calls ServiceStackHttpHandlerFactory.GetHandler
to get the handler for the current route.
ServiceStackHttpHandlerFactory.GetHandler
returns:
GetCatchAllHandlerIfAny(...),
if any.ServiceStackHttpHandlerFactory.GetHandlerForPathInfo
if any.ServiceStackHttpHandlerFactory.GetHandlerForPathInfo
returns:
GetCatchAllHandlerIfAny(...),
if any.GetCatchAllHandlerIfAny(...),
if any.The CatchAllHandlers array contains functions that evaluate the url and either return a handler, or null. The functions in the array are called in sequence and the first one that doesn't return null handles the route. Let me highlight some key elements:
First, the plugin adds a CatchAllHandler to the appHost.CatchAllHandlers
array when registered.
public void Register(IAppHost appHost)
{
appHost.CatchAllHandlers.Add((string method, string pathInfo, string filepath) =>
Factory(method, pathInfo, filepath));
}
Second, the CatchAllHandler. As described above, the function may be called for the domain root, an existing file or directory, or any other unmatched route. Your method should return a handler, if your criteria are met, or return 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 the wildcard at the root domain, you may not want to hijack routes that can be handled by another CatchAllHandler. If so, to avoid infinite recursion, you'll need a custom 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's the complete, and completely untested, plugin. It compiles. It carries no warranty of fitness for any specific 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;
}
}
}
ServiceStack now allows you to add a fallback route from the /
root path to handle any un-matched requests, that's not handled by a catch-all handler or refers to an existing static file. So in v3.9.56 you can now do:
[FallbackRoute("/{group}"]
public class Entity : IReturn<SomeType> {}
An alternative option is to register a IAppHost.CatchAllHandlers
to handle un-matched routes, but you would need to return your own IHttpHandler
to handle the request or alternatively return a RedirectHttpHandler
to redirect to a different route that is managed by ServiceStack.