Include the charset when DefaultHtmlRenderer sets the content type#15976
Include the charset when DefaultHtmlRenderer sets the content type#15976codeconsole wants to merge 3 commits into
Conversation
respond() rendering HTML set the response content type to the bare mime
type name ("text/html"), unlike the JSON and XML renderers, which run it
through GrailsWebUtil.getContentType to append the charset. A bare
content type leaves the response charset to the servlet container
(ISO-8859-1 on Tomcat), and GSP — which only applies its configured
UTF-8 content type when none is set yet — backs off, so the response
writer encodes Latin-1 and every non-Latin-1 character on the page is
written as a literal question mark.
The bug only bites requests whose Accept header negotiates to a concrete
HTML mime type: browsers always send one, while header-less clients
(curl and most HTTP test clients) negotiate MimeType.ALL, skip the
content-type stamp entirely, and get the correct UTF-8 page — which is
why it survived unnoticed. Historically the SiteMesh 2 filter also
masked it by unconditionally forcing UTF-8 onto decorated responses;
the SiteMesh 3 view-resolver integration respects the content type the
application sets, exposing the latent bug on every scaffolded page.
Mirror the JSON renderer: an encoding property defaulting to UTF-8,
applied through GrailsWebUtil.getContentType.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15976 +/- ##
=============================================
Coverage 49.6709% 49.6709%
- Complexity 17017 17019 +2
=============================================
Files 2004 2004
Lines 93896 93896
Branches 16448 16448
=============================================
Hits 46639 46639
+ Misses 40078 40077 -1
- Partials 7179 7180 +1
🚀 New features to boost your workflow:
|
|
|
||
| String suffix = '' | ||
|
|
||
| String encoding = GrailsWebUtil.DEFAULT_ENCODING |
There was a problem hiding this comment.
Shouldn't this be configurable and then this assigned to default if not set?
There was a problem hiding this comment.
Good call — done in 814b384. The encoding is now wired from configuration: DefaultRendererRegistry gained an encoding property that it propagates to every default renderer it creates (HTML, JSON, XML, and the Errors container renderers, plus the fallback HTML renderers the JSON/XML renderers construct when no view matches), and RestResponderGrailsPlugin populates it from the existing grails.converters.encoding setting, falling back to UTF-8 when unset. I reused that key rather than introducing a new one since these renderers are part of the same respond() pipeline the converters serve — one knob for response encoding. The setting is also documented in additional-spring-configuration-metadata.json so IDEs offer completion for it. Renderers registered as beans keep the independently settable encoding property. Covered by new specs at renderer level (custom encoding reaches the content type) and registry level (propagation to all default renderers).
There was a problem hiding this comment.
Shouldn't we default this to the configured value if set, otherwise use this default thought?
There was a problem hiding this comment.
Done in bb78ee0 — the property is now annotated @Value('${grails.converters.encoding:UTF-8}') so it resolves the configured value when set and falls back to UTF-8 otherwise, on the registry and all the renderer classes. The plugin-side wiring is removed.
|
Yes, that is a good suggestion. Making the encoding configurable while defaulting to grails-rest-transforms/src/main/groovy/org/grails/plugins/web/rest/render/html/DefaultHtmlRenderer.groovy |
Wire the existing grails.converters.encoding setting through DefaultRendererRegistry into every default renderer it creates — HTML, JSON, XML and the Errors container renderers — and into the fallback HTML renderers the JSON/XML renderers construct when no view matches, falling back to UTF-8 when unset. Renderers registered as beans keep their independently settable encoding property. Document the setting in the configuration metadata so IDEs offer it.
Replace the plugin-wired encoding property on DefaultRendererRegistry with @value('${grails.converters.encoding:UTF-8}') so the setting is driven by the environment for any registry or renderer registered as a Spring bean, not just the bean definition RestResponderGrailsPlugin happens to create. The same annotation now covers DefaultHtmlRenderer, DefaultJsonRenderer, DefaultXmlRenderer, AbstractLinkingRenderer and AbstractVndErrorRenderer, so custom renderer beans (HAL, Atom, Vnd.Error) pick the setting up automatically. The field default remains UTF-8 for renderers constructed outside Spring; the registry still propagates its encoding to the default renderers it instantiates itself.
|
Good call — moved the resolution into the classes themselves in bb78ee0. |
respond()rendering HTML sets the response content type to the bare mime type name (text/html) —DefaultHtmlRendereris the only renderer that skipsGrailsWebUtil.getContentType; its JSON and XML siblings both append the charset.Effect
A bare
text/htmlleaves the response charset to the servlet container (ISO-8859-1 on Tomcat). GSP only applies its configuredtext/html;charset=UTF-8when the response has no content type yet, so it backs off — the response writer then encodes Latin-1 and every non-Latin-1 character on the page is written as a literal?(Czech, Cyrillic, Thai, CJK; Latin-1-mappable accents survive, which makes the corruption easy to miss on Western-European pages).Why it went unnoticed
Acceptheader negotiates a concrete HTML mime type. Browsers always send one; curl and most HTTP test clients sendAccept: */*, which negotiatesMimeType.ALL, skips the content-type stamp, and gets a correct UTF-8 page.respond-rendered) page.Fix
Mirror
DefaultJsonRenderer: anencodingproperty defaulting toGrailsWebUtil.DEFAULT_ENCODING(UTF-8), applied viaGrailsWebUtil.getContentType(mimeType.name, encoding).Verification
text/html;charset=UTF-8on the negotiated render path (fails against the old code);:grails-rest-transforms:testand:grails-controllers:testgreen.Acceptheader serves all 18 localescharset=UTF-8, strict byte-validated (slovenčina,čeština,русский,中文intact); in Chrome,document.characterSetis nowUTF-8and the locale dropdown renders every native name correctly.Related: #15974, #15975.