Skip to content

Let Spring Boot register the character encoding filter - #16760

Open
codeconsole wants to merge 3 commits into
apache:9.0.xfrom
codeconsole:fix/15921-boot-encoding-filter-9.0.x
Open

codeconsole wants to merge 3 commits into
apache:9.0.xfrom
codeconsole:fix/15921-boot-encoding-filter-9.0.x

Conversation

@codeconsole

@codeconsole codeconsole commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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 logoutRequest from every POST), garbles every non-ASCII form field: a controller receives Café – 🂠for Café – 🍂.

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' characterEncodingFilter bean is a CharacterEncodingFilter too. That also means spring.servlet.encoding has no effect in a Grails app. Grails' filter does nothing Boot's doesn't: same class, and an encoding and a force flag that spring.servlet.encoding.charset and .force already cover. So:

  • ControllersAutoConfiguration no longer declares an encoding filter. Spring Boot's HttpEncodingAutoConfiguration registers one, configured by spring.servlet.encoding.
  • grails.filter.encoding and grails.filter.forceEncoding are deprecated. A new context initializer, GrailsCharacterEncodingInitializer, turns them into spring.servlet.encoding.charset / .force default properties and logs a deprecation warning. It runs after ExternalConfigRunListener, so the keys are translated from files named by grails.config.locations too, and before Boot binds spring.servlet.encoding. It translates a key only when the app doesn't set the Spring Boot property itself, so a spring.servlet.encoding setting the app makes wins, default properties included. The configuration metadata marks both keys deprecated, with their replacements.
  • Settings.FILTER_ENCODING, Settings.FILTER_FORCE_ENCODING and GrailsFilters.CHARACTER_ENCODING_FILTER are deprecated for removal.
  • The CAS plugin registers its single sign-out filter at Ordered.HIGHEST_PRECEDENCE + 1. A FilterRegistrationBean at 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-request is true when nothing is set). Grails' forceEncoding defaulted to false, so a request that declares its own charset is now decoded as UTF-8 too. spring.servlet.encoding.force-request: false restores the Grails 8 behavior. The upgrade guide says so.

Tests

  • ControllersAutoConfigurationSpec: with ControllersAutoConfiguration and HttpEncodingAutoConfiguration, there is exactly one CharacterEncodingFilter. It takes spring.servlet.encoding.charset and comes first in the filter chain.
  • GrailsCharacterEncodingInitializerSpec: translates both keys, the app's own spring.servlet.encoding wins, also when it is a default property, and nothing is added when neither key is set.
  • external-configuration example: LegacyEncodingConfigSpec starts the app with both deprecated keys in a grails.config.locations file (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.
  • app1: CharacterEncodingSpec posts a form, with no charset, through a filter at HIGHEST_PRECEDENCE + 1 that 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.
  • CAS example: CasSingleSignOutEncodingSpec posts Café – 🍂 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 with CasSingleSignOutSpec, which passes both ways, so single sign-out still works with its filter one order later. These also pass: the grails-controllers tests (232), the external-configuration example's integration tests, aggregateViolations (Checkstyle, CodeNarc, PMD and repository conventions, all clean) and rat.

Docs

  • The filter-ordering note in the plugin guide now says Spring Boot's encoding filter runs first. Its example moved to HIGHEST_PRECEDENCE + 1.
  • The upgrade guide has a new section, "20. Spring Boot Registers the Character Encoding Filter".

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 jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@testlens-app

testlens-app Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 20b06ed
▶️ Tests: 51968 executed
⚪️ Checks: 100/100 completed


Learn more about TestLens at testlens.app/docs.

@jamesfredley jamesfredley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants