8

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?

benjamin
  • 728
  • 8
  • 21

4 Answers4

10

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
  • 728
  • 8
  • 21
  • 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
2

Pretty sure this should work:

endpoints.MapFallbackToPage("{*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.

Darragh
  • 1,969
  • 1
  • 17
  • 25
1

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
  • 12,886
  • 4
  • 47
  • 56
  • 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
  • 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

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
  • 38,157
  • 20
  • 203
  • 266