Open
Description
Is there an existing issue for this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe the problem.
When no configured IRequestCultureProvider
(route, header, cookie, query, etc.) can determine a supported culture, the built-in RequestLocalizationMiddleware
silently falls back to the DefaultRequestCulture
. In some API or multi-tenant scenarios, falling back unexpectedly hides mis-configured clients or unsupported locales. I’d like an option to instead have the middleware fail fast—returning a 406 Not Acceptable—when no supported culture was found.
Describe the solution you'd like
- Add a new boolean property to
RequestLocalizationOptions
:
/// <summary>
/// Gets or sets a value indicating whether to set the request culture to the
/// <see cref="DefaultRequestCulture"/> when no supported culture can be determined
/// by the configured <see cref="IRequestCultureProvider"/>s (after any parent culture fallback).
/// Defaults to <c>true</c>.
/// </summary>
/// <remarks>
/// This setting only takes effect if none of the configured providers returns a supported culture
/// (and after parent culture fallback, if <see cref="FallBackToParentCultures"/> is <c>true</c>).
/// When <c>true</c>, the middleware will fall back to <see cref="DefaultRequestCulture"/>.
/// When <c>false</c>, the middleware will throw <see cref="RequestCultureNotSupportedException"/>,
/// terminating the pipeline.
/// </remarks>
/// <example>
/// If <see cref="FallBackToDefaultCulture"/> is <c>true</c> (default), and none of the
/// providers determines a supported culture, the request culture is set to the default
/// (e.g., "en-US"). If it is <c>false</c>, the middleware throws a
/// <see cref="RequestCultureNotSupportedException"/>, resulting in a 406 response.
/// </example>
public bool FallBackToDefaultCulture { get; set; } = true;
- Introduce a new exception
RequestCultureNotSupportedException
:
/// <summary>
/// Thrown only when no supported cultures could be determined
/// and <see cref="RequestLocalizationOptions.FallBackToDefaultCulture"/> is <c>false</c>.
/// </summary>
/// <remarks>
/// This exception indicates that the middleware was unable to match any of the
/// incoming culture values to the <see cref="RequestLocalizationOptions.SupportedCultures"/>
/// or <see cref="RequestLocalizationOptions.SupportedUICultures"/>, and default fallback
/// behavior has been disabled.
/// </remarks>
public class RequestCultureNotSupportedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="RequestCultureNotSupportedException"/> class.
/// </summary>
public RequestCultureNotSupportedException()
: base(Resources.Exception_RequestCultureNotSupported)
{
}
}
- Modify
RequestLocalizationMiddleware.Invoke(...)
to throw when strict mode is on:
/// <summary>
/// Invokes the logic of the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>A <see cref="Task"/> that completes when the middleware has completed processing.</returns>
public async Task Invoke(HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
RequestCulture? requestCulture = null;
IRequestCultureProvider? winningProvider = null;
if (_options.RequestCultureProviders != null)
{
foreach (var provider in _options.RequestCultureProviders)
{
var providerResultCulture = await provider.DetermineProviderCultureResult(context);
if (providerResultCulture == null)
{
continue;
}
var cultures = providerResultCulture.Cultures;
var uiCultures = providerResultCulture.UICultures;
CultureInfo? cultureInfo = null;
CultureInfo? uiCultureInfo = null;
if (_options.SupportedCultures != null)
{
cultureInfo = GetCultureInfo(
cultures,
_options.SupportedCultures,
_options.FallBackToParentCultures);
if (cultureInfo == null)
{
_logger.UnsupportedCultures(provider.GetType().Name, cultures);
}
}
if (_options.SupportedUICultures != null)
{
uiCultureInfo = GetCultureInfo(
uiCultures,
_options.SupportedUICultures,
_options.FallBackToParentUICultures);
if (uiCultureInfo == null)
{
_logger.UnsupportedUICultures(provider.GetType().Name, uiCultures);
}
}
if (cultureInfo == null && uiCultureInfo == null)
{
continue;
}
cultureInfo ??= _options.DefaultRequestCulture.Culture;
uiCultureInfo ??= _options.DefaultRequestCulture.UICulture;
var result = new RequestCulture(cultureInfo, uiCultureInfo);
requestCulture = result;
winningProvider = provider;
break;
}
}
// If we found a culture OR default-fallback is allowed, continue
if (_options.FallBackToDefaultCulture || requestCulture != null)
{
requestCulture ??= _options.DefaultRequestCulture;
context.Features.Set<IRequestCultureFeature>(new RequestCultureFeature(requestCulture, winningProvider));
SetCurrentThreadCulture(requestCulture);
if (_options.ApplyCurrentCultureToResponseHeaders)
{
var headers = context.Response.Headers;
headers.ContentLanguage = requestCulture.UICulture.Name;
}
await _next(context);
return;
}
// Strict mode: no culture and no fallback → fail fast
throw new RequestCultureNotSupportedException();
}
- Example:
var options = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = { new CultureInfo("fr-FR") },
SupportedUICultures = { new CultureInfo("fr-FR") },
FallBackToDefaultCulture = false
};
app.UseRequestLocalization(options);
Additional context
No response