19

I have a Blazor WebAssembly solution with a client project, server project and shared project, based on the default solution template from Microsoft. I'm editing and debugging in Visual Studio 2019 preview with Google Chrome.

Out-of-the-box, the solution has a single start-up project, which is the server application. That server application has a project reference to the client application. You can set it to use HTTPS by checking "Enable SSL" in the server project properties and I have done that.

When you click on debug it works perfectly.

Now I want to change it so that the Blazor WASM app only responds to requests from https://localhost:44331 and not requests to https://localhost:44331/api. These requests should be dealt with by API Controller endpoints of the server application instead. So, if somebody visits https://localhost:44331/api/something, and no such API endpoint exists, they should receive a 404 error code from the API and not be routed to the usual Blazor page saying "Sorry, there's nothing at this address."

I want to use this extra "/api" portion of the URL to keep the requests to the API separate from requests for pages. I think this will be closer to how a normal setup would be in production. I hope it's clear what I'm trying to do.

Here is a sample Controller declaration with route attribute:

namespace BlazorApp2.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        // Etc.

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            //etc.
        }
///etc.
    }
}

Here is what I have tried in my Startup.cs and it does not work. Can anybody suggest something that will please?

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Etc.
    app.UseStatusCodePages();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllers();
        // The line commented out below is the out-of-the-box behaviour for a Blazor WASM app with ASP NET Core API. This is the line I want to replace.
        // endpoints.MapFallbackToFile("index.html");

        // The line below is my (failed) attempt to get the behaviour I want.
        endpoints.MapFallback(HandleFallback);
    });
}

private async Task HandleFallback(HttpContext context)
{
    var apiPathSegment = new PathString("/api"); // Find out from the request URL if this is a request to the API or just a web page on the Blazor WASM app.

    bool isApiRequest = context.Request.Path.StartsWithSegments(apiPathSegment);

    if (!isApiRequest)
    {
        context.Response.Redirect("index.html"); // This is a request for a web page so just do the normal out-of-the-box behaviour.
    }
    else
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound; // This request had nothing to do with the Blazor app. This is just an API call that went wrong.
    }
}

Does anybody know how to get this working how I'd like, please?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
benjamin
  • 1,364
  • 1
  • 14
  • 26

7 Answers7

25

To recap the problem, when somebody makes a request to:

https://yourapp.com/api/someendpoint

and /api/someendpoint can't be found, they're taken to a Blazor page. This default behaviour is weird. For requests starting with /api, they were expecting an HTTP Status Code and probably a JSON object too, but instead, they got HTML. Maybe they don't even use your app. Maybe they're not even human (more likely they're a piece of software).

This is how you send them an HTTP Status Code instead. On your controllers:

[Route("api/[controller]")]
public class SampleController : ControllerBase
{
    // ...
}

In Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...
    app.UseStaticFiles();
    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
        endpoints.MapControllers();
        endpoints.Map("api/{**slug}", HandleApiFallback);
        endpoints.MapFallbackToFile("{**slug}", "index.html");
    });
}

private Task HandleApiFallback(HttpContext context)
{
    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
}
benjamin
  • 1,364
  • 1
  • 14
  • 26
  • I come late to this (I'm investigating another problem, anyway), but I think that a more flexible solution to this scenario is using something like: endpoints.MapFallbackToController("", "controller name"); – Andrea Oct 06 '20 at 09:30
  • Great question and answer! One nitpick, I'd suggest changing `return Task.FromResult(0);` to `return Task.CompletedTask;` as it's more clear and idiomatic. – Kirk Woll Feb 20 '21 at 14:38
  • 1
    That's a fair point. Thank you. I'll update it. – benjamin Feb 21 '21 at 04:23
  • The issue I'm seeming to face is that with this method, if you try to POST `api/path` rather than GET `api/path`, you're getting a 404 instead of a `MethodNotAllowed`. Any way to correct this? 404 is very unhelpful in debugging if you have the method wrong. – DLeh Nov 04 '21 at 18:35
  • i solved my issue here: https://stackoverflow.com/a/69844850/526704 – DLeh Nov 04 '21 at 19:32
  • 501 Not Implemented would be better – MarkB Aug 10 '23 at 19:50
9

Pretty sure this should work:

endpoints.MapFallbackToFile("{*path:regex(^(?!api).*$)}", "index.html"); // don't match paths beginning with api

I think it means something like only match URLs where the path does not start with api.

Buvy
  • 1,174
  • 12
  • 16
Darragh
  • 2,526
  • 1
  • 23
  • 31
  • With .NET 6 blazor I get the following error with this method: `ArgumentException: 'index.html' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/')` – DLeh Nov 04 '21 at 18:38
  • This is probably the best solution here only you need to use MapFallbackToFile as it is intended for static files rather than MapFallbackToPage which is intended to map to a Razor page. – Buvy Feb 16 '22 at 23:55
9

You can fix this by explicitly mapping Blazor fallback only for paths that don't start with /api and, then only mapping api paths for those that do start with /api, like I mention in this answer to my owner question. This gives the benefit that instead of just returning a 404 if you try to POST to a GET api method, you will get the proper api response of 405, or whatever other error the api would normally return given the request.

//explicitly only use blazor when the path doesn't start with api
app.MapWhen(ctx => !ctx.Request.Path.StartsWithSegments("/api"), blazor =>
{
    blazor.UseBlazorFrameworkFiles();
    blazor.UseStaticFiles();

    blazor.UseRouting();
    blazor.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("index.html");
    });
});

//explicitly map api endpoints only when path starts with api
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
    //if you are not using a blazor app, you can move these files out of this closure
    api.UseStaticFiles();
    api.UseRouting();
    api.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
});
DLeh
  • 23,806
  • 16
  • 84
  • 128
1

Using code from @Darragh I get the following error:

endpoints.MapFallbackToPage("{path:regex(^(?!api).$)}", "index.html");

System.ArgumentException: ''index.html' is not a valid page name. A page name is path relative to the Razor Pages root directory that starts with a leading forward slash ('/') and does not contain the file extension e.g "/Users/Edit". (Parameter 'pageName')'

enter image description here

The code will run if I use MapFallbackToFile instead of MapFallbackToPage like the original code.

However when I tested the regex it matched everything including an API URL:

https://regex101.com/r/nq7FCi/1

My Regex would look like this instead: ^(?!.*?(?:\/api\/)).*$ based on this answer:

https://stackoverflow.com/a/23207219/3850405

https://regex101.com/r/qmftyc/1

When testing this out It did not work anyway and urls containing /api/ was redirected to index.html.

My final code is based on @benjamin answer but with the original MapFallbackToFile used last.

app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllers();
    endpoints.Map("api/{**slug}", HandleApiFallback);
    endpoints.MapFallbackToFile("index.html");
});

private Task HandleApiFallback(HttpContext context)
{
    context.Response.StatusCode = StatusCodes.Status404NotFound;
    return Task.CompletedTask;
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • the regex should only be tested against the 'path' segment...not the absolute URL – Darragh Jun 22 '21 at 16:41
  • 4
    Can you explain what doe {**slug} stands for here ? – NSS Jun 24 '21 at 16:18
  • @NSS You can find some useful info form https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-template-reference – Poy Chang Oct 25 '21 at 20:34
  • The issue I'm seeming to face is that with this method, if you try to `POST api/path` rather than `GET api/path`, you're getting a 404 instead of a 1MethodNotAllowed`. Any way to correct this? 404 is very unhelpful in debugging if you have the http method wrong, or any other error that might happen gets overwritten by a 404 – DLeh Nov 04 '21 at 18:43
  • I solved my issue here: https://stackoverflow.com/a/69844850/526704 – DLeh Nov 04 '21 at 19:31
1

I have tried this with Blazor WebAssembly .NET 5. After publishing to IIS, the previously suggested solutions do not work.

The answer to this question is provided here. Tested and working.

Shortly:

Edit wwwroot\service-worker.published.js file and add the path to exclude, in this case /api/.

const shouldServeIndexHtml = event.request.mode === 'navigate' &&
                             !event.request.url.includes('/api/')
Connor Low
  • 5,900
  • 3
  • 31
  • 52
0

If you start from a Blazor WASM hosted solution, you'll a get a sample, just launch

dotnet new blazorwasm --hosted

It create a solution with 3 projects :

|-- Client
|-- Server
|-- Shared

In The Server Startup class, the middleware pipeline is setup like this :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseWebAssemblyDebugging();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseBlazorFrameworkFiles();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapFallbackToFile("index.html");
    });
}

The controller define its route with Route attribute. If you want to host the controller at /api/{controller} use the Route attribute value api/[controller]:

[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
agua from mars
  • 16,428
  • 4
  • 61
  • 70
  • Maybe I haven't explained this properly. Starting with this default template you posted above, but now you want your API endpoints to be served from URLs starting with "api" like https://localhost:44331/api/weatherforecasts and you want your Blazor pages to be served from URLs that do not start with "api" like https://localhost:44331/fetchdata. – benjamin Apr 17 '20 at 11:40
  • So, if somebody enters an API endpoint, you don't want the Blazor app to be involved at all. So visiting "https://localhost/4331/api/somethingnotthere" does not result in a Blazor page that says "Sorry, there's nothing at this address". It just results in a simple Http Status Code response of 404. I hope that's clear what the goal is now. – benjamin Apr 17 '20 at 11:40
  • Well, I don't get why you want this behavior but BTW you can wrote a middleware checking for unknown routes – agua from mars Apr 17 '20 at 12:55
  • 1
    Can you explain to me why you would want the default behaviour, please? The default behaviour is that somebody has made an API request, expecting an Http status code and probably a JSON object, but instead they ended up at an HTML page on a Blazor application. Maybe they never used your Blazor app before and were only interested in using your API. Maybe they aren't even a human;API requests can come from other software too. So, in my opinion, the default behaviour is weird, and the behaviour I'm trying to create is normal.... Am I wrong? – benjamin Apr 17 '20 at 13:33
  • Yeap, in that case write a middleware to check known URLs returning a 404 if the URL is not in a pre defined table. – agua from mars Apr 17 '20 at 13:57
0

With all other solutions applied, I still encounter the problem in ASP.NET Core 7 (not sure if the version matters) and it only happens on Production server. Turned out there's this difference between dev and production environment:

if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error"); // <-- This line
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

Removing app.UseExceptionHandler and I can see exceptions again.

Luke Vo
  • 17,859
  • 21
  • 105
  • 181