Sending accept-language with a quality value such as Accept-Language: en-us, en-BA;q=0.1 is part of the ietf standard in https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5

When this is sent and the code uses @RequestHeader(HttpHeaders.ACCEPT_LANGUAGE) Locale locale) the framework throws a MethodArgumentTypeMismatchException due to

https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/util/StringUtils.java#L935

    private static void validateLocalePart(String localePart) {
        for (int i = 0; i < localePart.length(); i++) {
            char ch = localePart.charAt(i);
            if (ch != ' ' && ch != '_' && ch != '-' && ch != '#' && !Character.isLetterOrDigit(ch)) {
                throw new IllegalArgumentException(
                        "Locale part \"" + localePart + "\" contains invalid characters");
            }
        }
    }

being called from https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/convert/support/StringToLocaleConverter.java#L41

There are easy workarounds such as simply removing the @RequestHeader annotation or using the Tomcat HttpServletRequest getLocale() method which uses https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/connector/Request.java#L2951

@RestController
public class TestLocale {

  private final HttpServletRequest httpServletRequest;

  @Autowired
  public TestLocale(final HttpServletRequest httpServletRequest) {
    this.httpServletRequest = httpServletRequest;
  }

  // Works with "en-us". Fails with "en-us, en-BA;q=0.1"
  @GetMapping(path = "/test-header")
  public String localeUsingHeader(@RequestHeader(HttpHeaders.ACCEPT_LANGUAGE) Locale locale) {
    return locale.toLanguageTag();
  }

  // Works with "en-us" and with "en-us, en-BA;q=0.1"
  @GetMapping(path = "/test-servlet")
  public String testServlet() {
    return httpServletRequest.getLocale().toLanguageTag();
  }

  // Works with "en-us" and with "en-us, en-BA;q=0.1"
  @GetMapping(path = "/test-locale")
  public String testLocale(Locale locale) {
    return locale.toLanguageTag();
  }
}

The full request being sent

curl --location 'http://localhost:8080/test-header' \
--header 'Content-Type: application/json' \
--header 'Accept-Language: en-us, en-BA;q=0.1' \
--header 'Accept: application/json'

Comment From: quaff

I think @RequestHeader works as expected, I don't think StringToLocaleConverter should accept value such as en-us, en-BA;q=0.1.

Comment From: NathanD001

@quaff why is that? As I posted, this conforms to the standard in https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5 and Spring is able to parse it fine if I remove @RequestHeader

  // Works with "en-us" and with "en-us, en-BA;q=0.1"
  @GetMapping(path = "/test-locale")
  public String testLocale(Locale locale) {
    return locale.toLanguageTag();
  }

Comment From: quaff

@quaff why is that? As I posted, this conforms to the standard in https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5 and Spring is able to parse it fine if I remove @RequestHeader

java // Works with "en-us" and with "en-us, en-BA;q=0.1" @GetMapping(path = "/test-locale") public String testLocale(Locale locale) { return locale.toLanguageTag(); }

From my understanding, spring will inject request.getLocale() in this case, and request.getLocale() is parsed by servlet container not spring itself. I think you are wrong to assert Accept-Language is identical to Locale, actually Locale is part of Accept-Language.

see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language

The HTTP Accept-Language request header indicates the natural language and locale that the client prefers.

Comment From: NathanD001

@quaff yes, Tomcat has a method to parse the Locale when a quality factor is sent https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/connector/Request.java#L2951 I don't see why Spring couldn't implement something similar. The Mozilla link you posted also agrees en-us, en-BA;q=0.1 is a valid value for accept-language

Comment From: Nicklas2751

@NathanD001 Wouldn't it a bug if spring just pick one locale of the given ones? For my understanding of the RFC, it must be a list of locales with their weights so an application could choose the best one it supports.

Comment From: NathanD001

@Nicklas2751 The Tomcat method parses the locales and sorts them by the quality factor https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/connector/Request.java#L2951

    protected void parseLocales() {

        localesParsed = true;

        // Store the accumulated languages that have been requested in
        // a local collection, sorted by the quality value (so we can
        // add Locales in descending order). The values will be ArrayLists
        // containing the corresponding Locales to be added
        TreeMap<Double,ArrayList<Locale>> locales = new TreeMap<>();

        Enumeration<String> values = getHeaders("accept-language");

        while (values.hasMoreElements()) {
            String value = values.nextElement();
            parseLocalesHeader(value, locales);
        }

        // Process the quality values in highest->lowest order (due to
        // negating the Double value when creating the key)
        for (ArrayList<Locale> list : locales.values()) {
            for (Locale locale : list) {
                addLocale(locale);
            }
        }
    }

Then retrieves the first element in the list https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/connector/Request.java#L1011

    @Override
    public Locale getLocale() {

        if (!localesParsed) {
            parseLocales();
        }

        if (locales.size() > 0) {
            return locales.get(0);
        }

        return defaultLocale;
    }

This seems like pretty reasonable default behavior instead of throwing an exception. The advantage of using Spring @RequestHeader instead of using the Tomcat method is that it's easier to generate swagger that way. If the dev wants a custom method to process each locale then they can still use a String and parse it themselves but this provides a default parsing method.

Comment From: bclozel

Removing the @RequestHeader annotation is not a workaround but the actual way to do this. Locales can be resolved from multiple places, including HTTP request headers, cookies, interceptors, etc. This is explained already in depth in the reference documentation for Locale support. Locale, TimeZone, ZoneId are supported method arguments for annotated controllers.

I don't think we should support this particular use case as locale and timezone management is more complex than this and we already have extensive support for it.