Swimburger

Use YARP to host client and API server on a single origin to avoid CORS

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

.NET Bot next to title: Use YARP to host client and API server on a single origin

Applications are often split up into a client and a server. The client renders the UI and communicates to the server through an API. But when the client and server are split up into separate projects and hosted at different origins (origin == scheme + host + port), it can be a hassle for the client to communicate with the server.

On the web specifically, communicating between different origins can be painful. By default, clients are not allowed to make ajax requests to a different origin. The server has to use Cross-Origin Resource Sharing (CORS) headers to allow cross-origin ajax requests.

Another way to resolve CORS challenges is to circumvent it entirely by having the client and server hosted on the same origin. If your technology stack allows for it, you can merge the client and server into a single project and host them on a single origin. If that's not possible, you can use a reverse proxy to consolidate the client origin and the server origin under the proxy's origin.

A diagram: web browser points to reverse proxy server. Reverse proxy server points to two web servers. The first web server is an API server. The second server is a frontend website server. The diagram visualizes how the HTTP requests from a web browser are sent to the reverse proxy. If the requested path starts with "/API/", the proxy forwards the request to the API server. Otherwise the request is sent to the Client Web Server which serves the frontend of your application.

The diagram above visualizes how the HTTP requests from a web browser are sent to the reverse proxy. If the requested path starts with '/API/', the proxy forwards the request to the API server. Otherwise, the request is sent to the Client Web Server which serves the frontend of your application.

You can pick any option for your frontend client, backend server, and reverse proxy server. It does not matter because they all understand the same language: HTTP. In this walkthrough, you'll learn how to communicate between a Blazor WebAssembly (WASM) client and an ASP.NET Web API without CORS. The client and server will be hosted on a single origin by putting Microsoft's new reverse proxy: YARP (which stands for "YARP: A Reverse Proxy")

A diagram: web browser points to the YARP reverse proxy. YARP server points to two web servers. The first web server is an ASP.NET Core WebAPI server. The second server is a Blazor WebAssembly server. The diagram visualizes how the HTTP requests from a web browser are sent to YARP. If the requested path starts with "/API/", the proxy forwards the request to the ASP.NET Core WebAPI server. Otherwise the request is sent to the Client Web Server which serves the Blazor WebAssembly application.

But why YARP instead of Apache or Nginx? YARP is built on top of ASP.NET Core which gives you some benefits:

  • You can configure YARP with any supported external configuration like JSON, environment variables, and many more. 
  • You can also use .NET code to configure and extend your reverse proxy.
  • You can store this piece of infrastructure in source control like any other ASP.NET Core project.
  • You can increase consistency between your local environment and server environments.
  • It's cross-platform, you can develop locally on your OS of choice but deploy it to your server OS of choice. For example, you can develop on Windows but use Linux machines for production.

Prerequisites #

To follow along you need the following tech:

You can find all the source code for the end result on GitHub.

Create the Blazor WASM client #

Before creating the client, create a folder where all projects will reside using the following commands:

mkdir YarpClientServerSingleOrigin
cd YarpClientServerSingleOrigin

Create a solution file using the dotnet command line interface (CLI):

dotnet new sln

Create the Blazor WASM client:

dotnet new blazorwasm -n Client -o Client

-n: specifies the name of the project

-o: specifies the location where the project will be created

Using the command above, you created a Blazor WASM project named 'Client' which is also used as the default namespace. This project is created in a subfolder also named 'Client'.

Add the client project to the solution file:

dotnet sln add Client

When you take a look at the FetchData component at Client\Pages\FetchData.razor, you can see the client is requesting data about the weather from a static JSON file:

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");

Update this line of code to fetch the data from api/WeatherForecast instead:

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/WeatherForecast");

This API endpoint does not exist yet, but you will create this soon.

Create the ASP.NET Core Web API server #

Create the Web API project using the dotnet CLI:

dotnet new webapi -n Server -o Server

Using the command above, you created a Web API project named 'Server' which is also used as the default namespace. This project is created in a subfolder also named 'Server'.

Add the server project to the solution file:

dotnet sln add Server

When you try to run the client and server at the same time, an error will be shown because they use the same port by default. The ports used by these projects are specified at 'Properties\launchSettings.json'. To prevent port collision, replace 'https://localhost:5001;http://localhost:5000' with 'https://localhost:5003;http://localhost:5002' at 'Server\Properties\launchSettings.json'. Or run these PowerShell commands:

$launchSettingsContents = Get-Content -path ./Server/Properties/launchSettings.json -Raw
$launchSettingsContents = $launchSettingsContents -replace 'https://localhost:5001;http://localhost:5000','https://localhost:5003;http://localhost:5002'
Set-Content -Value $launchSettingsContents -Path ./Server/Properties/launchSettings.json

There is a single controller that comes out of the box with the WebAPI template: WeatherForecastController.cs
This controller generates random weather forecasts and luckily for you matches the data model used in the Blazor WASM client. The WeatherForecast API can be requested at '/WeatherForecast', but the client will request it at '/api/WeatherForecast'. The reverse proxy you will create next will suffix the API with '/api'.

Create the YARP proxy #

Unlike some other reverse proxies, you cannot simply install it on your server and configure it. Instead, you have to create an empty ASP.NET Core Web project, add the YARP NuGet package, and configure YARP using configuration or code. Then you need to build, publish, and install it onto your server like any other ASP.NET Core application. This walkthrough will go through building and running this locally for development only.

Create an empty ASP.NET Core Web project using the dotnet CLI:

dotnet new web -n Proxy -o Proxy

Using the command above, you created an empty web project named 'Proxy' which is also used as the default namespace. This project is created in a subfolder also named 'Proxy'. In this case, Proxy is short for Reverse Proxy and not Forward Proxy.

Add the proxy project to the solution file:

dotnet sln add Proxy

To prevent port collision, you need to update the ports specified in 'launchSettings.json' just like the Web API project. Replace 'https://localhost:5001;http://localhost:5000' with 'https://localhost:5005;http://localhost:5004' at 'Proxy\Properties\launchSettings.json'. Or run these PowerShell commands:

$launchSettingsContents = Get-Content -path ./Proxy/Properties/launchSettings.json -Raw
$launchSettingsContents = $launchSettingsContents -replace 'https://localhost:5001;http://localhost:5000','https://localhost:5005;http://localhost:5004'
Set-Content -Value $launchSettingsContents -Path ./Proxy/Properties/launchSettings.json

Add the prelease NuGet package of YARP to the proxy project:

dotnet add Proxy package Microsoft.ReverseProxy --prerelease

The package 'Microsoft.ReverseProxy' is at version '1.0.0-preview.8.21065.1' at the time of writing this. If any of the upcoming functionality does not work as expected with newer versions of YARP, make sure to check out YARP's documentation.

Update the 'Startup.cs' file to enable YARP's functionality:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Proxy
{
    public class Startup
    {
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddReverseProxy()
                .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapReverseProxy();
            });
        }
    }
}

You can use any external configuration like 'appsettings.json' or environment variables to configure YARP. You can also configure the reverse proxy dynamically using code.

Update the 'Proxy\appsettings.Development.json' file:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ReverseProxy": {
    "Clusters": {
      "Client": {
        "Destinations": {
          "Client1": {
            "Address": "https://localhost:5001"
          }
        }
      },
      "Server": {
        "Destinations": {
          "Server1": {
            "Address": "https://localhost:5005"
          }
        }
      }
    },
    "Routes": [
      {
        "RouteId": "ServerRoute",
        "ClusterId": "Server",
        "Match": {
          "Path": "/api/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/api"
          }
        ]
      },
      {
        "RouteId": "ServerSwaggerRoute",
        "ClusterId": "Server",
        "Match": {
          "Path": "/swagger/{**catch-all}"
        }
      },
      {
        "RouteId": "ClientRoute",
        "ClusterId": "Client",
        "Match": {
          "Path": "{**catch-all}"
        }
      }
    ]
  }
}

The above JSON configures YARP as follows:

  • 2 clusters are defined
    1. A cluster named "Client"
      • Client cluster only has one destination named "Client1" which points to the local development URL for the Blazor WASM client.
    2. A cluster named "Server"
      • Server cluster only has one destination named "Server1" which points to the local development URL for the Web API server.
  • 3 routes are defined
    1. A route named "ServerRoute" forwards HTTP requests to the "Server" cluster any time the requested path starts with '/api/'. Before the HTTP request is forwarded to the Web API, the '/api' prefix is removed by the "PathRemovePrefix" transform.
    2. A route named "ServerSwaggerRoute" fixes the Swagger UI for the Web API. The Swagger UI isn't fully aware it is now hosted under the '/api' suffix and this extra route fixes some of those HTTP requests made to the wrong path.
    3. A route name "ClientRoute" forwards HTTP requests to the "Client" cluster for all remaining requests.

In this case, there is only one destination per cluster, but YARP can handle multiple destinations and the reverse proxy will load balance the HTTP requests.

Testing the result #

You need to open three different terminals. One for every project. Run the following commands:

  • Terminal 1: dotnet run -p Client
  • Terminal 2: dotnet run -p Server
  • Terminal 3: dotnet run -p Proxy

Open the browser and navigate to the Proxy which should be accessible at 'https://localhost:5005'.

The Blazor client should be showing up. Click on the "Fetch Data" navigation link.

The weather forecast data should populate just as you expect it from an out of the box Blazor WASM project. But every time you reload, the data should be different because it is being generated by the Web API project.

You can write a script to bring up and down all three projects instead of manually running them in three different terminals. OR you could use Microsoft's experimental project Tye to tie everything together.

Install Tye as a .NET global tool:

dotnet tool install -g Microsoft.Tye --version "0.5.0-alpha.20555.1"

You can find the most recent version of Tye at Nuget.

Initialize tye using this command:

tye init

This will create a configuration YAML file 'tye.yaml'. Update the YAML file to match the content below:

name: yarpclientserversingleorigin
services:
- name: client
  project: Client/Client.csproj
  bindings:
  - port: 5001
    protocol: https
- name: server
  project: Server/Server.csproj
  bindings:
  - port: 5003
    protocol: https
- name: proxy
  project: Proxy/Proxy.csproj

Tye will automatically use random ports for projects if you do not specify them. The Proxy project expects the client and server project to be run on port 5001 and 5003. Hence you need to explicitly configure which port and protocol these projects use in the YAML file.

Run this command to bring up all projects at once with tye:

tye run

You can find the proxy URL in the output, or you can navigate to tye's dashboard at localhost:8000

Screenshot of the project Tye dashboard

In addition to finding the URL for each service, you can also watch the log streams of each service.

Summary #

API's need to provide CORS headers to explicitly allow ajax requests from web clients hosted on a different origin. Reverse proxies can merge the API origin and client origin onto a single origin. Using Microsoft's new reverse proxy "YARP", you configured the proxy to forward requests to '/api' to the Web API, and all other requests to the Blazor WASM client.
Using project Tye, you tied all projects together so you can easily run all projects at once instead of starting them in 3 different terminals.

You can learn more about YARP at YARP's Documentation and you can learn more about project Tye on its GitHub repository.

Related Posts

Related Posts