From 2d4e3b2c5486d881b498874356ca42ac13dbbcc8 Mon Sep 17 00:00:00 2001 From: John Niang Date: Thu, 22 May 2025 11:14:00 +0800 Subject: [PATCH] Fix the problem of undetermined locale (#7458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 解析错误导致无法访问页面的问题 ``` --- .../webfilter/LocaleChangeWebFilter.java | 11 ++++-- .../app/theme/ThemeLocaleContextResolver.java | 5 +++ .../theme/ThemeLocaleContextResolverTest.java | 9 +++++ .../webfilter/LocaleChangeWebFilterTest.java | 37 ++++++++++++++++++- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/application/src/main/java/run/halo/app/infra/webfilter/LocaleChangeWebFilter.java b/application/src/main/java/run/halo/app/infra/webfilter/LocaleChangeWebFilter.java index 3233d4ad5..9f25e98b0 100644 --- a/application/src/main/java/run/halo/app/infra/webfilter/LocaleChangeWebFilter.java +++ b/application/src/main/java/run/halo/app/infra/webfilter/LocaleChangeWebFilter.java @@ -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 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())) diff --git a/application/src/main/java/run/halo/app/theme/ThemeLocaleContextResolver.java b/application/src/main/java/run/halo/app/theme/ThemeLocaleContextResolver.java index 3d23b2f64..da6b608a0 100644 --- a/application/src/main/java/run/halo/app/theme/ThemeLocaleContextResolver.java +++ b/application/src/main/java/run/halo/app/theme/ThemeLocaleContextResolver.java @@ -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); diff --git a/application/src/test/java/run/halo/app/theme/ThemeLocaleContextResolverTest.java b/application/src/test/java/run/halo/app/theme/ThemeLocaleContextResolverTest.java index a1ada782e..2e5cc3985 100644 --- a/application/src/test/java/run/halo/app/theme/ThemeLocaleContextResolverTest.java +++ b/application/src/test/java/run/halo/app/theme/ThemeLocaleContextResolverTest.java @@ -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( diff --git a/application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java b/application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java index 4d336bc4a..f11db04da 100644 --- a/application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java +++ b/application/src/test/java/run/halo/app/webfilter/LocaleChangeWebFilterTest.java @@ -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")