mirror of https://github.com/halo-dev/halo
Fix the problem of undetermined locale (#7458)
#### What type of PR is this? /kind bug /area core /milestone 2.21.x #### What this PR does / why we need it: This PR check if the locale is undetermined during resolving locale. Or it will cause the error below if locale is `und`: ```java 2025-05-21T17:28:45.953+08:00 ERROR 58760 --- [undedElastic-14] o.s.w.s.adapter.HttpWebHandlerAdapter : [c1824fa5-1] 500 Server Error for HTTP GET "/" org.thymeleaf.exceptions.TemplateOutputException: An error happened during template rendering at org.thymeleaf.engine.OutputTemplateHandler.handleText(OutputTemplateHandler.java:75) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.AbstractTemplateHandler.handleText(AbstractTemplateHandler.java:221) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.handleText(ProcessorTemplateHandler.java:587) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.Text.beHandled(Text.java:97) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.Model.process(Model.java:300) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.GatheringModelProcessable.process(GatheringModelProcessable.java:78) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.queueProcessable(ProcessorTemplateHandler.java:2106) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.handleCloseElement(ProcessorTemplateHandler.java:1642) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.CloseElementTag.beHandled(CloseElementTag.java:139) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.Model.process(Model.java:300) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.OpenElementTagModelProcessable.process(OpenElementTagModelProcessable.java:110) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.queueProcessable(ProcessorTemplateHandler.java:2106) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1559) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:155) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ThrottledTemplateProcessor.process(ThrottledTemplateProcessor.java:235) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ThrottledTemplateProcessor.process(ThrottledTemplateProcessor.java:200) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.spring6.SpringWebFluxTemplateEngine$StreamThrottledTemplateProcessor.process(SpringWebFluxTemplateEngine.java:720) ~[thymeleaf-spring6-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.spring6.SpringWebFluxTemplateEngine.lambda$createChunkedStream$2(SpringWebFluxTemplateEngine.java:269) ~[thymeleaf-spring6-3.1.3.RELEASE.jar:3.1.3.RELEASE] at reactor.core.publisher.FluxGenerate$GenerateSubscription.slowPath(FluxGenerate.java:271) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxGenerate$GenerateSubscription.request(FluxGenerate.java:213) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.requestUpstream(FluxSubscribeOn.java:131) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.onSubscribe(FluxSubscribeOn.java:124) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:178) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxGenerate.subscribe(FluxGenerate.java:85) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.InternalFluxOperator.subscribe(InternalFluxOperator.java:68) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.publisher.FluxSubscribeOn$SubscribeOnSubscriber.run(FluxSubscribeOn.java:194) ~[reactor-core-3.7.5.jar:3.7.5] at reactor.core.scheduler.BoundedElasticThreadPerTaskScheduler$SchedulerTask.run(BoundedElasticThreadPerTaskScheduler.java:1013) ~[reactor-core-3.7.5.jar:3.7.5] at java.base/java.lang.VirtualThread.run(VirtualThread.java:329) ~[na:na] Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Locale "" cannot be used as it does not specify a language. (template: "modules/layout" - line 12, col 49) at org.thymeleaf.messageresolver.StandardMessageResolutionUtils.computeMessageResourceNamesFromBase(StandardMessageResolutionUtils.java:202) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.messageresolver.StandardMessageResolutionUtils.resolveMessagesForTemplate(StandardMessageResolutionUtils.java:69) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.messageresolver.StandardMessageResolver.resolveMessagesForTemplate(StandardMessageResolver.java:380) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.messageresolver.StandardMessageResolver.resolveMessage(StandardMessageResolver.java:282) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.messageresolver.StandardMessageResolver.resolveMessage(StandardMessageResolver.java:227) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.context.AbstractEngineContext.getMessage(AbstractEngineContext.java:134) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.standard.expression.MessageExpression.executeMessageExpression(MessageExpression.java:265) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:69) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.standard.expression.Expression.execute(Expression.java:109) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.standard.expression.Expression.execute(Expression.java:138) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.standard.processor.StandardUtextTagProcessor.doProcess(StandardUtextTagProcessor.java:87) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.processor.element.AbstractElementTagProcessor.process(AbstractElementTagProcessor.java:95) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.util.ProcessorConfigurationUtils$ElementTagProcessorWrapper.process(ProcessorConfigurationUtils.java:633) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.ProcessorTemplateHandler.handleOpenElement(ProcessorTemplateHandler.java:1314) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.OpenElementTag.beHandled(OpenElementTag.java:205) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.TemplateModel.process(TemplateModel.java:136) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.TemplateManager.process(TemplateManager.java:519) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.util.LazyProcessingCharSequence.writeUnresolved(LazyProcessingCharSequence.java:85) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.util.AbstractLazyCharSequence.write(AbstractLazyCharSequence.java:103) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.AbstractTextualTemplateEvent.writeContent(AbstractTextualTemplateEvent.java:224) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.Text.write(Text.java:78) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] at org.thymeleaf.engine.OutputTemplateHandler.handleText(OutputTemplateHandler.java:71) ~[thymeleaf-3.1.3.RELEASE.jar:3.1.3.RELEASE] ... 29 common frames omitted ``` #### Does this PR introduce a user-facing change? ```release-note 修复因 Locale 解析错误导致无法访问页面的问题 ```pull/7466/head
parent
79a4386c82
commit
2d4e3b2c54
|
@ -3,6 +3,7 @@ package run.halo.app.infra.webfilter;
|
||||||
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;
|
import static run.halo.app.theme.ThemeLocaleContextResolver.LANGUAGE_COOKIE_NAME;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
@ -45,20 +46,24 @@ public class LocaleChangeWebFilter implements WebFilter {
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||||
var request = exchange.getRequest();
|
|
||||||
return matcher.matches(exchange)
|
return matcher.matches(exchange)
|
||||||
.filter(MatchResult::isMatch)
|
.filter(MatchResult::isMatch)
|
||||||
.doOnNext(result -> {
|
.doOnNext(result -> {
|
||||||
var localeContext = themeLocaleContextResolver.resolveLocaleContext(exchange);
|
var localeContext = themeLocaleContextResolver.resolveLocaleContext(exchange);
|
||||||
var locale = localeContext.getLocale();
|
var locale = localeContext.getLocale();
|
||||||
if (locale != null) {
|
if (locale != null) {
|
||||||
setLanguageCookie(exchange, locale);
|
setLanguageCookieIfAbsent(exchange, locale);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(Mono.defer(() -> chain.filter(exchange)));
|
.then(Mono.defer(() -> chain.filter(exchange)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLanguageCookie(ServerWebExchange exchange, Locale locale) {
|
void setLanguageCookieIfAbsent(ServerWebExchange exchange, Locale locale) {
|
||||||
|
var languageCookie = exchange.getRequest().getCookies().getFirst(LANGUAGE_COOKIE_NAME);
|
||||||
|
if (languageCookie != null
|
||||||
|
&& Objects.equals(languageCookie.getValue(), locale.toLanguageTag())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var cookie = ResponseCookie.from(LANGUAGE_COOKIE_NAME, locale.toLanguageTag())
|
var cookie = ResponseCookie.from(LANGUAGE_COOKIE_NAME, locale.toLanguageTag())
|
||||||
.path("/")
|
.path("/")
|
||||||
.secure("https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))
|
.secure("https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.LocaleUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.context.i18n.LocaleContext;
|
import org.springframework.context.i18n.LocaleContext;
|
||||||
import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;
|
import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;
|
||||||
|
@ -39,6 +40,10 @@ public class ThemeLocaleContextResolver extends AcceptHeaderLocaleContextResolve
|
||||||
.or(() -> UserLocaleRequestAttributeWriteFilter.getUserLocale(request))
|
.or(() -> UserLocaleRequestAttributeWriteFilter.getUserLocale(request))
|
||||||
.orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());
|
.orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());
|
||||||
|
|
||||||
|
if (LocaleUtils.isLanguageUndetermined(locale)) {
|
||||||
|
locale = null;
|
||||||
|
}
|
||||||
|
|
||||||
var timeZone = getTimeZoneFromCookie(request)
|
var timeZone = getTimeZoneFromCookie(request)
|
||||||
.orElseGet(TimeZone::getDefault);
|
.orElseGet(TimeZone::getDefault);
|
||||||
|
|
||||||
|
|
|
@ -177,6 +177,15 @@ class ThemeLocaleContextResolverTest {
|
||||||
assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);
|
assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isEqualTo(US);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveUnderminedLocale() {
|
||||||
|
var request = MockServerHttpRequest.get("/")
|
||||||
|
.header(HttpHeaders.ACCEPT_LANGUAGE, "und")
|
||||||
|
.build();
|
||||||
|
var exchange = MockServerWebExchange.from(request);
|
||||||
|
|
||||||
|
assertThat(this.resolver.resolveLocaleContext(exchange).getLocale()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
private ServerWebExchange exchange(Locale... locales) {
|
private ServerWebExchange exchange(Locale... locales) {
|
||||||
return MockServerWebExchange.from(
|
return MockServerWebExchange.from(
|
||||||
|
|
|
@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.springframework.http.HttpCookie;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
|
@ -44,11 +45,43 @@ class LocaleChangeWebFilterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRespondLanguageCookieWithUndefinedLanguageTag() {
|
void shouldNotRespondLanguageCookieIfChanged() {
|
||||||
WebFilterChain webFilterChain = filterExchange -> {
|
WebFilterChain webFilterChain = filterExchange -> {
|
||||||
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
|
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
|
||||||
assertNotNull(languageCookie);
|
assertNotNull(languageCookie);
|
||||||
assertEquals("und", languageCookie.getValue());
|
assertEquals("zh-CN", languageCookie.getValue());
|
||||||
|
return Mono.empty();
|
||||||
|
};
|
||||||
|
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
|
||||||
|
.accept(MediaType.TEXT_HTML)
|
||||||
|
.cookie(new HttpCookie("language", "zh-HK"))
|
||||||
|
.queryParam("language", "zh-CN")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
this.filter.filter(exchange, webFilterChain).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRespondLanguageCookieIfNotChanged() {
|
||||||
|
WebFilterChain webFilterChain = filterExchange -> {
|
||||||
|
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
|
||||||
|
assertNull(languageCookie);
|
||||||
|
return Mono.empty();
|
||||||
|
};
|
||||||
|
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
|
||||||
|
.accept(MediaType.TEXT_HTML)
|
||||||
|
.cookie(new HttpCookie("language", "zh-CN"))
|
||||||
|
.queryParam("language", "zh-CN")
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
this.filter.filter(exchange, webFilterChain).block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRespondLanguageCookieWithUndeterminedLanguageTag() {
|
||||||
|
WebFilterChain webFilterChain = filterExchange -> {
|
||||||
|
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
|
||||||
|
assertNull(languageCookie);
|
||||||
return Mono.empty();
|
return Mono.empty();
|
||||||
};
|
};
|
||||||
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
|
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
|
||||||
|
|
Loading…
Reference in New Issue