SPA fallback without FallEnableSpaFallback

Hi,
I’ve tried using SharpPagesFeature with EnableSpaFallback = false, as we are already handing the fallback more selectively in the .Net Middleware layer.
However, some routes are still being served by SharpPagesFeature, in particular /.well-known/change-password and for that matter any /.well-known/.. path always returns the SPA fallback page.

This is surprising because EnableSpaFallback = false is specified,
and because any path starting with /.well-known should never return a SPA fallback page, regardless of EnableSpaFallback.

For that matter /.really/now also returns the SPA fallback.

I think that any /.well-known page should never return the SPA fallback page (wwwroot/index.html), regardless of EnableSpaFallback

As a workaround we have used

var sharpPagesFeature = new SharpPagesFeature
{
    EnableSpaFallback = false,
};
sharpPagesFeature.IgnorePaths.Add("/.well-known");
self.Plugins.Add(sharpPagesFeature);

EnableSpaFallback is already false by default, so setting it to false doesn’t do anything different. When set to true, it’s the same behavior as if you had defined your own Fallback Route.

What may be happening is that the request is being handled by the Page Based Routing which adds a fallback handler to check if the route matches a routing page.

You can disable this with:

Plugins.Add(new SharpPagesFeature
{
    DisablePageBasedRouting = true,
});

But isn’t the Page Based routing logic somewhat broken when
/.well-known/change-password is routed to wwwroot/index.html?

I did try with

            Plugins.Add(new SharpPagesFeature{
                DisablePageBasedRouting = true,
                EnableSpaFallback = false,
            });

and that did not change the behaviour.

For instance /.well-known/web-app-origin-association also responds with wwwroot/index.html, and that seems plain wrong as well.

The project I’m testing on is made with x new script and then modified TestStack/Startup.cs to add the parameters mentioned above.

It shouldn’t be routed, I’m unclear as to what’s serving the request, do you have your own [FallbackRoute] defined yourself? Or maybe a registered 404 handler?

The project has no special handler, it is really clean.
Recipe:

mkdir TestStack
cd TestStack
x new script
patch -p1 <<EOS
diff --git a/TestStack/Startup.cs b/TestStack/Startup.cs
index a19e7e7..00474e2 100644
--- a/TestStack/Startup.cs
+++ b/TestStack/Startup.cs
@@ -47,7 +47,11 @@ namespace TestStack
         // Configure your AppHost with the necessary configuration and dependencies your App needs
         public override void Configure(Container container)
         {
-            Plugins.Add(new SharpPagesFeature()); // enable server-side rendering, see: https://sharpscript.net/docs/sharp-pages
+            Plugins.Add(new SharpPagesFeature
+            {
+                DisablePageBasedRouting = true,
+                EnableSpaFallback = false,
+            }); // enable server-side rendering, see: https://sharpscript.net/docs/sharp-pages
 
             SetConfig(new HostConfig
             {

EOS
cd TestStack
dotnet run

Then try in a browser or command line.

Running curl -i https://localhost:5001/ returns the index.html as expected.

Running curl -i https://localhost:5001/some/where returns a 200, with a page not found message. I think it should have returned a 404 code, but with the same body.

HTTP/1.1 200 OK
Date: Fri, 15 Oct 2021 12:52:56 GMT
Content-Type: text/html
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE HTML>
<html>
    <head>
        <title>Page Not Found</title>

Running curl -i https://localhost:5001/.some/where returns a 200 with the index.html page - the misbehaviour I’m talking about. It should have returned a 404 Not Found code.

HTTP/1.1 200 OK
Date: Fri, 15 Oct 2021 12:55:14 GMT
Content-Type: text/html
Server: Kestrel
Transfer-Encoding: chunked
Set-Cookie: ss-pid=tlyukMraQAfetAbBqNso; expires=Tue, 15 Oct 2041 12:55:14 GMT; path=/; secure; samesite=lax; httponly
Set-Cookie: ss-id=QJenKAhcfdr9ZlGrvsXh; path=/; secure; samesite=lax; httponly

<!DOCTYPE html>
<html lang="en">
<head>
    <title>#Script Pages</title>

This is because of the registered NotFound HttpHandler where it will render the /notfound.html for 404 Responses of HTML requests.

The other was an issue with /.path/info requests where it treated it as a file extension and resolved the file name to the / index page. This is now resolved from the latest v5.12+ that’s available on MyGet.

Where it should work as expected if you clear your NuGet packages cache and re-run the project:

$ nuget locals all -clear

Great with a fix!

The only remaining issue is the return of a Not Found page, using a 200 OK code. This would lead the client to try to parse the document as a success document, possibly also indexing any links on the Not Found page.

The handler is great, but even if it successfully renders a page, shouldn’t it preserve the correct HTML response code?

The NotFound HTML Page is only returned for HTML Requests, a 404 is still returned for API Responses (e.g. json).

But a 404 can be returned for the html as well? Consistent with the API.
Why is that not done?
How may I work around that, to use the html handler, with a custom 404 page, and still return the correct response code?

The SharpPageHandler just renders the page, i.e. doesn’t set the StatusCode, you could probably use the ValidateFn to set it, e.g:

appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] = 
    new SharpPageHandler("/notfound") {
        ValidateFn = req => {
            req.Response.StatusCode = 404;
            return true;
        }
    };

Thank you for the workaround.
Without any registered appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] it works as expected:

curl -s -i https://localhost:5001/some/where | head -n 20
HTTP/1.1 404 Not Found
Date: Mon, 18 Oct 2021 08:55:48 GMT
Server: Kestrel
Content-Length: 0

With the default handler (in the template from ServiceStack), the response code is changed incorrectly. So

appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] = new SharpPageHandler("/notfound");

gives

curl -s -i https://localhost:5001/some/where | head -n 20
HTTP/1.1 200 OK
Date: Mon, 18 Oct 2021 08:54:42 GMT
Content-Type: text/html
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE HTML>
<html>
    <head>
        <title>Page Not Found</title>

When using your workaround

appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] =
    new SharpPageHandler("/notfound")
    {
        ValidateFn = req =>
        {
            req.Response.StatusCode = 404;
            return true;
        }
    };

It works the way it should

curl -s -i https://localhost:5001/some/where | head -n 20
HTTP/1.1 404 Not Found
Date: Mon, 18 Oct 2021 08:51:45 GMT
Content-Type: text/html
Server: Kestrel
Transfer-Encoding: chunked

<!DOCTYPE HTML>
<html>
    <head>
        <title>Page Not Found</title>

So the SharpPageHandler overrides the response code it is registered for, and changes the 404 into 200, and that is both wrong and surprising.
I think that either the examples should be updated to add your workaround, or that the SharpPageHandler should receive the original http response code, and preserve it.

On that note, the same is the case for the appHost.CustomErrorHttpHandlers[HttpStatusCode.Forbidden] = new SharpPageHandler("/forbidden");.
It should return a 403 response code for html as well as the API, but it returns a 200 instead:

curl -s -i https://localhost:5001/hello | head -n 20     
HTTP/1.1 200 OK
Date: Mon, 18 Oct 2021 09:08:21 GMT
Content-Type: text/plain
Server: Kestrel
Transfer-Encoding: chunked
Set-Cookie: ss-id=42jDIrssfMDonvo1L8L1; path=/; secure; samesite=lax; httponly
Set-Cookie: ss-pid=Loky3Oi2FqjJzoGYJovf; expires=Fri, 18 Oct 2041 09:08:21 GMT; path=/; secure; samesite=lax; httponly
Vary: Accept
X-Powered-By: ServiceStack/5.121 NetCore/OSX

Forbidden

So the same fix in ServiceStack, or modification of templates to correctly set the response code, should be applied as well.

So in summary, if you experience incorrect response code for html pages, you get a 200 OK when you expected a 404 Not Found or a 403 Forbidden, and you have CustomErrorHttpHandlers using SharpPageHandler, then use the following workaround from @mythz instead of the suggestions in the ServiceStack templates.

public void Configure(IAppHost appHost)
{
    appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] =
        new SharpPageHandler("/notfound")
        {
            ValidateFn = req =>
            {
                req.Response.StatusCode = (int)HttpStatusCode.NotFound;
                return true;
            }
        };
    appHost.CustomErrorHttpHandlers[HttpStatusCode.Forbidden] =
        new SharpPageHandler("/forbidden")
        {
            ValidateFn = req =>
            {
                req.Response.StatusCode = (int)HttpStatusCode.NotFound;
                return true;
            }
        };
}

Thank you for your help @mythz.

I’ve just added a more appropriate Filter which should make this a little nicer, e.g:

appHost.CustomErrorHttpHandlers[HttpStatusCode.NotFound] =
    new SharpPageHandler("/notfound") {
        Filter = req => req.Response.StatusCode = (int)HttpStatusCode.NotFound;
    };

Available in the latest v5.12+ that’s now available on MyGet.

Nice with proper filtering for the SharpPageHandler :slight_smile:

I hope it makes it to the templates as well :slight_smile:

Since you are looking at the SharpPageHandler, could the CustomErrorHttpHandlers have set an optional CustomStatusCode parameter in the SharpPageHandler, that would be used by the PageResult if specified?

CustomErrorHttpHandlers is a generic Dictionary<HttpStatusCode, IServiceStackHandler> for registering any kind of Http Handler. The Filter is flexible, explicit and intuitive.

1 Like