ServiceStack REST API path variables from root throwing exception

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 : IReturn<SomeType> {}

This throws a NotSupported Exception “RestPath ‘/{collection}’ on type Entity is not supported”. However, if I change the path as follows (along with the associated path in AppHost configuration) to:
[Route("/rest/{group}"]

It works just fine. In order to integrate with the system that I am working with, I need to use /{group}.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 1

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.

Method 2

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:

  1. A matching RawHttpHandler, if any.
  2. If the domain root, the handler returned by GetCatchAllHandlerIfAny(...), if any.
  3. If the route matches a metadata uri (I’m skipping over the exact logic here, as it’s not important for your question), the relevant handler, if any.
  4. The handler returned by ServiceStackHttpHandlerFactory.GetHandlerForPathInfo if any.
  5. NotFoundHandler.

ServiceStackHttpHandlerFactory.GetHandlerForPathInfo returns:

  1. If the url matches a valid REST route, a new RestHandler.
  2. If the url matches an existing file or directory, it returns
    • the handler returned by GetCatchAllHandlerIfAny(...), if any.
    • If it’s a supported filetype, a StaticFileHandler,
    • If it’s not a supported filetype, the ForbiddenHttpHandler.
  3. The handler returned by GetCatchAllHandlerIfAny(...), if any.
  4. null.

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

}       
}


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x