Using @RequestHeader(ACCEPT_LANGUAGE) Locale locale) throws MethodArgumentTypeMismatchException when a quality value is sent
NathanD001 opened this issue · 7 comments
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
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'
I think @RequestHeader
works as expected, I don't think StringToLocaleConverter
should accept value such as en-us, en-BA;q=0.1
.
@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();
}
@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(); }
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.
@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
@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.
@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.
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.