Skip to content

Add FallBackToDefaultCulture option to disable default‐culture fallback in RequestLocalizationMiddleware #61784

Open
@GiviKDev

Description

@GiviKDev

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

  1. 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;
  1. 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)
    {
    }
}
  1. 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();
}
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-middlewareIncludes: URL rewrite, redirect, response cache/compression, session, and other general middlewaresfeature-localization

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions