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 java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
@ -45,20 +46,24 @@ public class LocaleChangeWebFilter implements WebFilter {
|
|||
@Override
|
||||
@NonNull
|
||||
public Mono<Void> filter(ServerWebExchange exchange, @NonNull WebFilterChain chain) {
|
||||
var request = exchange.getRequest();
|
||||
return matcher.matches(exchange)
|
||||
.filter(MatchResult::isMatch)
|
||||
.doOnNext(result -> {
|
||||
var localeContext = themeLocaleContextResolver.resolveLocaleContext(exchange);
|
||||
var locale = localeContext.getLocale();
|
||||
if (locale != null) {
|
||||
setLanguageCookie(exchange, locale);
|
||||
setLanguageCookieIfAbsent(exchange, locale);
|
||||
}
|
||||
})
|
||||
.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())
|
||||
.path("/")
|
||||
.secure("https".equalsIgnoreCase(exchange.getRequest().getURI().getScheme()))
|
||||
|
|
|
@ -4,6 +4,7 @@ import java.util.Locale;
|
|||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.LocaleUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.i18n.LocaleContext;
|
||||
import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext;
|
||||
|
@ -39,6 +40,10 @@ public class ThemeLocaleContextResolver extends AcceptHeaderLocaleContextResolve
|
|||
.or(() -> UserLocaleRequestAttributeWriteFilter.getUserLocale(request))
|
||||
.orElseGet(() -> super.resolveLocaleContext(exchange).getLocale());
|
||||
|
||||
if (LocaleUtils.isLanguageUndetermined(locale)) {
|
||||
locale = null;
|
||||
}
|
||||
|
||||
var timeZone = getTimeZoneFromCookie(request)
|
||||
.orElseGet(TimeZone::getDefault);
|
||||
|
||||
|
|
|
@ -177,6 +177,15 @@ class ThemeLocaleContextResolverTest {
|
|||
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) {
|
||||
return MockServerWebExchange.from(
|
||||
|
|
|
@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
|
@ -44,11 +45,43 @@ class LocaleChangeWebFilterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void shouldRespondLanguageCookieWithUndefinedLanguageTag() {
|
||||
void shouldNotRespondLanguageCookieIfChanged() {
|
||||
WebFilterChain webFilterChain = filterExchange -> {
|
||||
var languageCookie = filterExchange.getResponse().getCookies().getFirst("language");
|
||||
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();
|
||||
};
|
||||
var exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/home")
|
||||
|
|
Loading…
Reference in New Issue