Skip to content

Commit cd8a1bd

Browse files
committed
AcceptHeaderLocaleContextResolver leniently handles invalid header value
Also falls back to language-only match among its supported locales now. Issue: SPR-16500 Issue: SPR-16457
1 parent 067ad4c commit cd8a1bd

File tree

6 files changed

+139
-66
lines changed

6 files changed

+139
-66
lines changed

spring-web/src/main/java/org/springframework/http/HttpHeaders.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -473,6 +473,7 @@ public void setAcceptLanguage(List<Locale.LanguageRange> languages) {
473473
* a list of supported locales you can pass the returned list to
474474
* {@link Locale#filter(List, Collection)}.
475475
* @since 5.0
476+
* @throws IllegalArgumentException if the value cannot be converted to a language range
476477
*/
477478
public List<Locale.LanguageRange> getAcceptLanguage() {
478479
String value = getFirst(ACCEPT_LANGUAGE);
@@ -494,6 +495,7 @@ public void setAcceptLanguageAsLocales(List<Locale> locales) {
494495
* {@link java.util.Locale.LanguageRange} to a {@link Locale}.
495496
* @return the locales or an empty list
496497
* @since 5.0
498+
* @throws IllegalArgumentException if the value cannot be converted to a locale
497499
*/
498500
public List<Locale> getAcceptLanguageAsLocales() {
499501
List<Locale.LanguageRange> ranges = getAcceptLanguage();
@@ -879,7 +881,7 @@ public void setDate(long date) {
879881
* by the {@code Date} header.
880882
* <p>The date is returned as the number of milliseconds since
881883
* January 1, 1970 GMT. Returns -1 when the date is unknown.
882-
* @throws IllegalArgumentException if the value can't be converted to a date
884+
* @throws IllegalArgumentException if the value cannot be converted to a date
883885
*/
884886
public long getDate() {
885887
return getFirstDate(DATE);

spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java

+39-31
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,19 +23,21 @@
2323
import org.springframework.context.i18n.LocaleContext;
2424
import org.springframework.context.i18n.SimpleLocaleContext;
2525
import org.springframework.http.HttpHeaders;
26-
import org.springframework.http.server.reactive.ServerHttpRequest;
2726
import org.springframework.lang.Nullable;
27+
import org.springframework.util.CollectionUtils;
28+
import org.springframework.util.StringUtils;
2829
import org.springframework.web.server.ServerWebExchange;
2930

3031
/**
3132
* {@link LocaleContextResolver} implementation that simply uses the primary locale
3233
* specified in the "Accept-Language" header of the HTTP request (that is,
3334
* the locale sent by the client browser, normally that of the client's OS).
3435
*
35-
* <p>Note: Does not support {@code setLocale}, since the accept header
36+
* <p>Note: Does not support {@link #setLocaleContext}, since the accept header
3637
* can only be changed through changing the client's locale settings.
3738
*
3839
* @author Sebastien Deleuze
40+
* @author Juergen Hoeller
3941
* @since 5.0
4042
*/
4143
public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver {
@@ -51,11 +53,9 @@ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver
5153
* determined via {@link HttpHeaders#getAcceptLanguageAsLocales()}.
5254
* @param locales the supported locales
5355
*/
54-
public void setSupportedLocales(@Nullable List<Locale> locales) {
56+
public void setSupportedLocales(List<Locale> locales) {
5557
this.supportedLocales.clear();
56-
if (locales != null) {
57-
this.supportedLocales.addAll(locales);
58-
}
58+
this.supportedLocales.addAll(locales);
5959
}
6060

6161
/**
@@ -82,42 +82,50 @@ public Locale getDefaultLocale() {
8282
return this.defaultLocale;
8383
}
8484

85+
8586
@Override
8687
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
87-
ServerHttpRequest request = exchange.getRequest();
88-
List<Locale> acceptableLocales = request.getHeaders().getAcceptLanguageAsLocales();
89-
if (this.defaultLocale != null && acceptableLocales.isEmpty()) {
90-
return new SimpleLocaleContext(this.defaultLocale);
91-
}
92-
Locale requestLocale = acceptableLocales.isEmpty() ? null : acceptableLocales.get(0);
93-
if (isSupportedLocale(requestLocale)) {
94-
return new SimpleLocaleContext(requestLocale);
88+
List<Locale> requestLocales = null;
89+
try {
90+
requestLocales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
9591
}
96-
Locale supportedLocale = findSupportedLocale(request);
97-
if (supportedLocale != null) {
98-
return new SimpleLocaleContext(supportedLocale);
92+
catch (IllegalArgumentException ex) {
93+
// Invalid Accept-Language header: treat as empty for matching purposes
9994
}
100-
return (this.defaultLocale != null ? new SimpleLocaleContext(this.defaultLocale) :
101-
new SimpleLocaleContext(requestLocale));
95+
return new SimpleLocaleContext(resolveSupportedLocale(requestLocales));
10296
}
10397

104-
private boolean isSupportedLocale(@Nullable Locale locale) {
105-
if (locale == null) {
106-
return false;
98+
@Nullable
99+
private Locale resolveSupportedLocale(@Nullable List<Locale> requestLocales) {
100+
if (CollectionUtils.isEmpty(requestLocales)) {
101+
return this.defaultLocale; // may be null
102+
}
103+
List<Locale> supported = getSupportedLocales();
104+
if (supported.isEmpty()) {
105+
return requestLocales.get(0); // never null
107106
}
108-
List<Locale> supportedLocales = getSupportedLocales();
109-
return (supportedLocales.isEmpty() || supportedLocales.contains(locale));
110-
}
111107

112-
@Nullable
113-
private Locale findSupportedLocale(ServerHttpRequest request) {
114-
List<Locale> requestLocales = request.getHeaders().getAcceptLanguageAsLocales();
108+
Locale languageMatch = null;
115109
for (Locale locale : requestLocales) {
116-
if (getSupportedLocales().contains(locale)) {
110+
if (supported.contains(locale)) {
111+
// Full match: typically language + country
117112
return locale;
118113
}
114+
else if (languageMatch == null) {
115+
// Let's try to find a language-only match as a fallback
116+
for (Locale candidate : supported) {
117+
if (!StringUtils.hasLength(candidate.getCountry()) &&
118+
candidate.getLanguage().equals(locale.getLanguage())) {
119+
languageMatch = candidate;
120+
}
121+
}
122+
}
119123
}
120-
return null;
124+
if (languageMatch != null) {
125+
return languageMatch;
126+
}
127+
128+
return (this.defaultLocale != null ? this.defaultLocale : requestLocales.get(0));
121129
}
122130

123131
@Override

spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,12 +26,11 @@
2626
import org.springframework.web.server.ServerWebExchange;
2727

2828
/**
29-
* {@link LocaleContextResolver} implementation that always returns
30-
* a fixed default locale and optionally time zone.
31-
* Default is the current JVM's default locale.
29+
* {@link LocaleContextResolver} implementation that always returns a fixed locale
30+
* and optionally time zone. Default is the current JVM's default locale.
3231
*
33-
* <p>Note: Does not support {@code setLocale(Context)}, as the fixed
34-
* locale and time zone cannot be changed.
32+
* <p>Note: Does not support {@link #setLocaleContext}, as the fixed locale and
33+
* time zone cannot be changed.
3534
*
3635
* @author Sebastien Deleuze
3736
* @since 5.0
@@ -71,6 +70,7 @@ public FixedLocaleContextResolver(Locale locale, @Nullable TimeZone timeZone) {
7170
this.timeZone = timeZone;
7271
}
7372

73+
7474
@Override
7575
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
7676
return new TimeZoneAwareLocaleContext() {

spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java

+62-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,37 +22,39 @@
2222

2323
import org.junit.Test;
2424

25+
import org.springframework.http.HttpHeaders;
2526
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
2627
import org.springframework.mock.web.test.server.MockServerWebExchange;
2728
import org.springframework.web.server.ServerWebExchange;
2829

2930
import static java.util.Locale.*;
30-
import static org.junit.Assert.assertEquals;
31+
import static org.junit.Assert.*;
3132

3233
/**
3334
* Unit tests for {@link AcceptHeaderLocaleContextResolver}.
3435
*
3536
* @author Sebastien Deleuze
37+
* @author Juergen Hoeller
3638
*/
3739
public class AcceptHeaderLocaleContextResolverTests {
3840

39-
private AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver();
41+
private final AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver();
4042

4143

4244
@Test
43-
public void resolve() throws Exception {
45+
public void resolve() {
4446
assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
4547
assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale());
4648
}
4749

4850
@Test
49-
public void resolvePreferredSupported() throws Exception {
51+
public void resolvePreferredSupported() {
5052
this.resolver.setSupportedLocales(Collections.singletonList(CANADA));
5153
assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale());
5254
}
5355

5456
@Test
55-
public void resolvePreferredNotSupported() throws Exception {
57+
public void resolvePreferredNotSupported() {
5658
this.resolver.setSupportedLocales(Collections.singletonList(CANADA));
5759
assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, UK)).getLocale());
5860
}
@@ -61,14 +63,65 @@ public void resolvePreferredNotSupported() throws Exception {
6163
public void resolvePreferredNotSupportedWithDefault() {
6264
this.resolver.setSupportedLocales(Arrays.asList(US, JAPAN));
6365
this.resolver.setDefaultLocale(JAPAN);
66+
assertEquals(JAPAN, this.resolver.resolveLocaleContext(exchange(KOREA)).getLocale());
67+
}
68+
69+
@Test
70+
public void resolvePreferredAgainstLanguageOnly() {
71+
this.resolver.setSupportedLocales(Collections.singletonList(ENGLISH));
72+
assertEquals(ENGLISH, this.resolver.resolveLocaleContext(exchange(GERMANY, US, UK)).getLocale());
73+
}
74+
75+
@Test
76+
public void resolveMissingAcceptLanguageHeader() {
77+
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
78+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
79+
assertNull(this.resolver.resolveLocaleContext(exchange).getLocale());
80+
}
6481

65-
MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(KOREA).build();
82+
@Test
83+
public void resolveMissingAcceptLanguageHeaderWithDefault() {
84+
this.resolver.setDefaultLocale(US);
85+
86+
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
87+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
88+
assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale());
89+
}
90+
91+
@Test
92+
public void resolveEmptyAcceptLanguageHeader() {
93+
MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "").build();
6694
MockServerWebExchange exchange = MockServerWebExchange.from(request);
67-
assertEquals(JAPAN, this.resolver.resolveLocaleContext(exchange).getLocale());
95+
assertNull(this.resolver.resolveLocaleContext(exchange).getLocale());
96+
}
97+
98+
@Test
99+
public void resolveEmptyAcceptLanguageHeaderWithDefault() {
100+
this.resolver.setDefaultLocale(US);
101+
102+
MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "").build();
103+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
104+
assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale());
105+
}
106+
107+
@Test
108+
public void resolveInvalidAcceptLanguageHeader() {
109+
MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "en_US").build();
110+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
111+
assertNull(this.resolver.resolveLocaleContext(exchange).getLocale());
112+
}
113+
114+
@Test
115+
public void resolveInvalidAcceptLanguageHeaderWithDefault() {
116+
this.resolver.setDefaultLocale(US);
117+
118+
MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "en_US").build();
119+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
120+
assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale());
68121
}
69122

70123
@Test
71-
public void defaultLocale() throws Exception {
124+
public void defaultLocale() {
72125
this.resolver.setDefaultLocale(JAPANESE);
73126
MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
74127
MockServerWebExchange exchange = MockServerWebExchange.from(request);

spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java

+26-14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package org.springframework.web.server.i18n;
218

319
import java.time.ZoneId;
@@ -12,10 +28,8 @@
1228
import org.springframework.mock.web.test.server.MockServerWebExchange;
1329
import org.springframework.web.server.ServerWebExchange;
1430

15-
import static java.util.Locale.CANADA;
16-
import static java.util.Locale.FRANCE;
17-
import static java.util.Locale.US;
18-
import static org.junit.Assert.assertEquals;
31+
import static java.util.Locale.*;
32+
import static org.junit.Assert.*;
1933

2034
/**
2135
* Unit tests for {@link FixedLocaleContextResolver}.
@@ -24,32 +38,30 @@
2438
*/
2539
public class FixedLocaleContextResolverTests {
2640

27-
private FixedLocaleContextResolver resolver;
28-
2941
@Before
3042
public void setup() {
3143
Locale.setDefault(US);
3244
}
3345

3446
@Test
3547
public void resolveDefaultLocale() {
36-
this.resolver = new FixedLocaleContextResolver();
37-
assertEquals(US, this.resolver.resolveLocaleContext(exchange()).getLocale());
38-
assertEquals(US, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
48+
FixedLocaleContextResolver resolver = new FixedLocaleContextResolver();
49+
assertEquals(US, resolver.resolveLocaleContext(exchange()).getLocale());
50+
assertEquals(US, resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
3951
}
4052

4153
@Test
4254
public void resolveCustomizedLocale() {
43-
this.resolver = new FixedLocaleContextResolver(FRANCE);
44-
assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange()).getLocale());
45-
assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
55+
FixedLocaleContextResolver resolver = new FixedLocaleContextResolver(FRANCE);
56+
assertEquals(FRANCE, resolver.resolveLocaleContext(exchange()).getLocale());
57+
assertEquals(FRANCE, resolver.resolveLocaleContext(exchange(CANADA)).getLocale());
4658
}
4759

4860
@Test
4961
public void resolveCustomizedAndTimeZoneLocale() {
5062
TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("UTC"));
51-
this.resolver = new FixedLocaleContextResolver(FRANCE, timeZone);
52-
TimeZoneAwareLocaleContext context = (TimeZoneAwareLocaleContext)this.resolver.resolveLocaleContext(exchange());
63+
FixedLocaleContextResolver resolver = new FixedLocaleContextResolver(FRANCE, timeZone);
64+
TimeZoneAwareLocaleContext context = (TimeZoneAwareLocaleContext) resolver.resolveLocaleContext(exchange());
5365
assertEquals(FRANCE, context.getLocale());
5466
assertEquals(timeZone, context.getTimeZone());
5567
}

spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,9 @@ public class AcceptHeaderLocaleResolver implements LocaleResolver {
5555
* @param locales the supported locales
5656
* @since 4.3
5757
*/
58-
public void setSupportedLocales(@Nullable List<Locale> locales) {
58+
public void setSupportedLocales(List<Locale> locales) {
5959
this.supportedLocales.clear();
60-
if (locales != null) {
61-
this.supportedLocales.addAll(locales);
62-
}
60+
this.supportedLocales.addAll(locales);
6361
}
6462

6563
/**

0 commit comments

Comments
 (0)