dotnet / dotnet/aspnetcore

Consider adding a middleware to redirect to a preferred host

Open
#60,674 2 comments 1 reaction 0 assignees View on GitHub
api-suggestion area-middleware
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 6h
Merged PRs (30d)
290

Description

## Problem

I recently deployed a new domain name for an existing site and found myself having to write a custom middleware to handle the transition. The site is still listening on both domains (e.g. `olddomain.com` and `newdomain.com`) but requests to `olddomain.com` now redirect to `newdomain.com`.

This feels like it would be a pretty common scenario (site listening on multiple hosts/domains but one is preferred).

ASP.NET Core contains multiple features related to URL rewriting/redirecting and host matching, but none of them actually address this scenario directly. The [URL rewriting middleware](https://learn.microsoft.com/aspnet/core/fundamentals/url-rewriting) only has first-class support for path rewriting (not host). [Host filtering](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/host-filtering) is about limiting which specific host names are allowed, but doesn't do redirection. The most similar scenario is HTTPS redirection for which we have a specific dedicated middleware for.

## Proposal

We should consider adding a new middleware, `PreferredHostRedirectionMiddleware`, that can be used to facilitate this scenario. This middleware can be added to the pipeline, much like the HTTPS redirection middleware, and it will redirect requests to a preferred host if one is configured.

The middleware should support direct configuration, e.g. passing a preferred host in as a string, or via the configuration/options system.

### Example Usage

```csharp
var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.UsePreferredHostRedirection("newdomain.com");
app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, World!");

app.Run();
```

## Strawman

Here's the implementation from the site I did this on recently. It doesn't use `IOptions` but rather it just looks at configuration directly but of course that can be changed. It also supports setting a query string flag on the new URL so that a message can be displayed on the site after the redirect. This likely wouldn't be something baked in, but perhaps a delegate could be set on the middleware options to allow modifying the URL being redirected to in order to enable scenarios like that.

```csharp
using System.Net;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;

namespace MySite.Middleware;

public class PreferredHostRedirectionMiddleware(ILogger logger, IConfiguration configuration) : IMiddleware
{
public const string PreferredHostKey = "PreferredHost";
public const string RedirectedKey = "r";
public const string TrueValue = "true";

private readonly ILogger _logger = logger;
private readonly string? _preferredHost = configuration[PreferredHostKey];

public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!string.IsNullOrEmpty(_preferredHost) && !string.Equals(context.Request.Host.Host, _preferredHost, StringComparison.Ordinal))
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Request host '{RequestHost}' is not the preferred host. Redirecting to preferred host '{PreferredHost}'.",
context.Request.Host.Host, _preferredHost);
}

var redirectUriBuilder = new UriBuilder(context.Request.GetDisplayUrl()) { Host = _preferredHost };

if (context.GetEndpoint() is not null)
{
// Request is for an endpoint (e.g. a page) so set query string to ensure notification is displayed
var query = QueryHelpers.ParseQuery(redirectUriBuilder.Query);
query.Remove(RedirectedKey);
query.Add(RedirectedKey, TrueValue);
redirectUriBuilder.Query = QueryHelpers.AddQueryString("", query);
}

var redirectUrl = redirectUriBuilder.ToString();

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Will redirect to URL '{RedirectUrl}'.", redirectUrl);
}

context.Response.Redirect(redirectUrl);
return Task.CompletedTask;
}

return next(context);
}
}

public static class PreferredHostRedirectionExtensions
{
public static IServiceCollection AddPreferredHostRedirection(this IServiceCollection services)
{
services.AddTransient();

return services;
}

public static IApplicationBuilder UsePreferredHostRedirection(this IApplicationBuilder app)
{
var logger = app.ApplicationServices.GetRequiredService().CreateLogger(nameof(PreferredHostRedirectionExtensions));
var configuration = app.ApplicationServices.GetRequiredService();
var preferredHost = configuration[PreferredHostRedirectionMiddleware.PreferredHostKey];

if (string.IsNullOrEmpty(preferredHost))
{
return app;
}

if (!Uri.CheckHostName(preferredHost).Equals(UriHostNameType.Dns))
{
if (logger.IsEnabled(LogLevel.Error))
{
logger.LogError("The preferred host is not a valid DNS name: {PreferredHost}", preferredHost);
}
return app;
}

app.UseMiddleware();

return app;
}
}
```

*appsettings.json*

```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "olddomain.com;newdomain.com",
"PreferredHost": "newdomain.com"
}
```

*appsettings.Development.json*

```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "127.0.0.1;localhost",
"PreferredHost": "localhost"
}
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.