Repository navigation
Let Spring Boot register the character encoding filter - #16760
Open
codeconsole wants to merge 3 commits into
Open
codeconsole wants to merge 3 commits into
codeconsole wants to merge 3 commits into
Conversation
Grails registered its own encoding filter at order -180. It made Spring Boot's back off, so spring.servlet.encoding had no effect, and a filter ordered ahead of it that read a request parameter, such as the CAS plugin's single sign-out filter, which reads logoutRequest from every POST, made the servlet container decode the body as ISO-8859-1. Spring Boot's filter is the same class, runs at Ordered.HIGHEST_PRECEDENCE, and its properties cover both Grails settings. Drop Grails' filter, translate the deprecated grails.filter.encoding and grails.filter.forceEncoding into spring.servlet.encoding with a warning, and register the single sign-out filter right after Spring Boot's. Fixes apache#15921
jamesfredley
left a comment
Contributor
There was a problem hiding this comment.
Not approving. There is one encoding filter, it runs first, and 9.0.x is the right line for the forced-request change. The deprecated keys are not translated when they come from grails.config.locations, and they can overwrite an explicit Spring default.
DefaultPropertiesPropertySource.addOrMerge replaces existing keys, so an application that set spring.servlet.encoding.charset through SpringApplication.setDefaultProperties and still had grails.filter.encoding got the deprecated value. Translate a deprecated key only when its Spring Boot property is not set, and say in the warning that it is ignored otherwise.
…s loaded The translation ran as an environment post-processor, but ExternalConfigRunListener loads the files named by grails.config.locations after the post-processors have run. grails.filter.encoding and grails.filter.forceEncoding set in such a file were never translated, so the application got UTF-8 and a forced request encoding instead, and Latin-1 form input was corrupted. Translate them in an ApplicationContextInitializer instead, which runs after every run listener and before Spring Boot binds spring.servlet.encoding. The external-configuration example now starts the application with both keys in a grails.config.locations file and checks the encoding filter.
codeconsole
requested review from
jamesfredley,
jdaugherty,
matrei and
sbglasius
October 25, 2026 16:39
✅ All tests passed ✅🏷️ Commit: 20b06ed Learn more about TestLens at testlens.app/docs. |
jamesfredley
approved these changes
Oct 25, 2026
jamesfredley
left a comment
Contributor
There was a problem hiding this comment.
Approving. The deprecated encoding keys are translated after grails.config.locations loads, and an application's own spring.servlet.encoding default is kept. The two earlier comments are resolved. 9.0.x is the right line for the forced-request change.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #15921 on 9.0.x. #16759 fixes it on 7.0.x by moving Grails' encoding filter to
Ordered.HIGHEST_PRECEDENCE. For 9.0 this PR removes Grails' filter and lets Spring Boot's do the job.Once any filter reads a request parameter, the servlet container decodes the request body, and it uses ISO-8859-1 unless a character encoding has been set by then. Grails registers its encoding filter at order -180, so a filter ordered ahead of it that reads a parameter, such as the CAS plugin's single sign-out filter (it reads
logoutRequestfrom every POST), garbles every non-ASCII form field: a controller receivesCafé – ðŸ‚forCafé – 🍂.Spring Boot already avoids this. It registers its encoding filter at
Ordered.HIGHEST_PRECEDENCE, but in a Grails app its filter backs off, because Grails'characterEncodingFilterbean is aCharacterEncodingFiltertoo. That also meansspring.servlet.encodinghas no effect in a Grails app. Grails' filter does nothing Boot's doesn't: same class, and an encoding and a force flag thatspring.servlet.encoding.charsetand.forcealready cover. So:ControllersAutoConfigurationno longer declares an encoding filter. Spring Boot'sHttpEncodingAutoConfigurationregisters one, configured byspring.servlet.encoding.grails.filter.encodingandgrails.filter.forceEncodingare deprecated. A new context initializer,GrailsCharacterEncodingInitializer, turns them intospring.servlet.encoding.charset/.forcedefault properties and logs a deprecation warning. It runs afterExternalConfigRunListener, so the keys are translated from files named bygrails.config.locationstoo, and before Boot bindsspring.servlet.encoding. It translates a key only when the app doesn't set the Spring Boot property itself, so aspring.servlet.encodingsetting the app makes wins, default properties included. The configuration metadata marks both keys deprecated, with their replacements.Settings.FILTER_ENCODING,Settings.FILTER_FORCE_ENCODINGandGrailsFilters.CHARACTER_ENCODING_FILTERare deprecated for removal.Ordered.HIGHEST_PRECEDENCE + 1. AFilterRegistrationBeanat the same order as Boot's filter would still run first; Run the character encoding filter ahead of every other filter #16759 checks this on 7.0.x.Behavior change: Spring Boot forces the request encoding by default (
force-requestis true when nothing is set). Grails'forceEncodingdefaulted to false, so a request that declares its own charset is now decoded as UTF-8 too.spring.servlet.encoding.force-request: falserestores the Grails 8 behavior. The upgrade guide says so.Tests
ControllersAutoConfigurationSpec: withControllersAutoConfigurationandHttpEncodingAutoConfiguration, there is exactly oneCharacterEncodingFilter. It takesspring.servlet.encoding.charsetand comes first in the filter chain.GrailsCharacterEncodingInitializerSpec: translates both keys, the app's ownspring.servlet.encodingwins, also when it is a default property, and nothing is added when neither key is set.LegacyEncodingConfigSpecstarts the app with both deprecated keys in agrails.config.locationsfile (ISO-8859-1,forceEncoding: false) and checks that the encoding filter uses them. When the keys were translated by an environment post-processor, it failed: the filter had UTF-8 and forced the request encoding.CharacterEncodingSpecposts a form, with no charset, through a filter atHIGHEST_PRECEDENCE + 1that reads one of its parameters, and checks that the filter and the controller both get the text as typed. This is the same reproducer as in Run the character encoding filter ahead of every other filter #16759.CasSingleSignOutEncodingSpecpostsCafé – 🍂to an echo action with single sign-out enabled, against the CAS container.Without the change, both the app1 spec (
Café – ðŸ‚) and the CAS spec fail; with it, both pass. I ran the CAS spec with-DTESTCONFIG=cas, together withCasSingleSignOutSpec, which passes both ways, so single sign-out still works with its filter one order later. These also pass: thegrails-controllerstests (232), the external-configuration example's integration tests,aggregateViolations(Checkstyle, CodeNarc, PMD and repository conventions, all clean) andrat.Docs
HIGHEST_PRECEDENCE + 1.