codeconsole opened a new pull request, #15758:
URL: https://github.com/apache/grails-core/pull/15758
## Summary
This modernizes Grails' content-negotiation defaults for Grails 8, in two
related parts:
1. **MIME types are now framework defaults.** New applications no longer
need a `grails.mime.types` block in `application.yml`; the framework supplies
the full set out of the box, and any configuration you provide is merged over
it.
2. **The `Accept` header is honored for all clients by default.** The legacy
"ignore the `Accept` header for browser user agents" behavior is now opt-in.
This fixes browser `fetch()`/`axios` calls that request `application/json` but
receive HTML.
Both changes are backward compatible for typical apps and are covered by
upgrade notes.
---
## Part 1 — MIME types as framework defaults
Previously, every generated app carried a ~15-line `grails.mime.types`
block, while the framework's built-in fallback (`MimeType.createDefaults()`)
only knew 6 types. Removing the block silently shrank content negotiation to
those 6.
This change:
- Expands `MimeType.createDefaults()` to the full set new apps historically
declared: `all, atom, css, csv, form, html, js, json, multipartForm, pdf, rss,
text, hal, xml`.
- Changes `MimeTypesConfiguration` to **merge** a user's `grails.mime.types`
over the defaults instead of replacing them. A declared extension overrides the
default for that extension; extensions you don't mention keep their defaults;
declared extensions are ordered first, so the documented "first entry is the
default format" contract is preserved.
- Stops generating the block from Grails Forge (GSP / JSON / Markup view
features) and from the `web` and `rest-api` profiles.
Result: a new app's `application.yml` is cleaner, and adding one custom type
no longer wipes the rest.
---
## Part 2 — Honor the `Accept` header by default (stop User-Agent sniffing)
### What changed
`grails.mime.disable.accept.header.userAgents` previously defaulted to
`['Gecko', 'WebKit', 'Presto', 'Trident']`, which caused Grails to **ignore the
`Accept` header** for any request whose `User-Agent` matched a major browser
engine. It now defaults to **unset**, so the `Accept` header is honored for
every client. The browser blocklist remains available as an explicit opt-in.
### Why the old default is wrong today
**It works around a problem that no longer exists.** The blocklist dates to
the Firefox 2/3 era, when browsers sent:
```
text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
```
— ranking XML *above* HTML. Modern browsers send a well-formed header that
ranks HTML highest:
```
Chrome / Firefox / Safari:
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
```
`text/html` is q=1.0 and `application/xml` is q=0.9, so ordinary q-value
negotiation already resolves browser navigations to HTML. No sniffing required.
**It actively breaks modern AJAX.** The old code exempted "XHR" requests —
but only those carrying `X-Requested-With: XMLHttpRequest`, a header **jQuery**
adds automatically and **`fetch()` / `axios` do not**. So in a current SPA:
```js
fetch('/api/thing', { headers: { Accept: 'application/json' } })
```
matched the browser `User-Agent`, the `Accept` header was discarded before
it was even read, and the response came back as HTML — even though the client
explicitly asked for JSON. That is the concrete bug this fixes.
### How Spring Boot / Spring MVC handles it
Spring takes the simpler, opposite approach: **it trusts the client's
explicit signals and never inspects `User-Agent`.**
- Spring MVC's `ContentNegotiationManager` resolves the representation from
the `Accept` header (and optionally an explicit `format` parameter). There is
no User-Agent strategy.
- A `@RestController` method with `Accept: application/json` selects its
`HttpMessageConverter` purely from the `Accept` header → JSON, regardless of
which browser made the request.
- Spring has moved *further* toward relying only on explicit signals:
path-extension content negotiation was disabled by default (`favorPathExtension
= false`) in Spring Framework 5.3+ specifically because deriving the response
type from an implicit signal (the URL suffix) enabled Reflected File Download
(RFD) attacks.
Grails 8 now aligns with Spring: honor the explicit `Accept` header; don't
branch on an ambient, spoofable one.
### Security / robustness considerations of User-Agent-based negotiation
Beyond correctness, negotiating on `User-Agent` has real downsides:
- **Cache confusion / poisoning.** When a single URL returns different
representations based on `User-Agent`, a correct implementation must emit
`Vary: User-Agent`. Grails' UA-based negotiation does not, so a shared / CDN
cache keyed on the URL can store one representation (say HTML) and serve it to
a client that needs another (a JSON API consumer), or the reverse. Honoring
`Accept` removes this class of cache mismatch from the default configuration.
- **Spoofable, non-contractual signal.** `User-Agent` is entirely
client-controlled. Gating the response *type* on it makes server behavior
depend on a value the client can set arbitrarily — fragile to reason about and
harder to cover with security tooling / WAF rules that expect a stable request
→ representation contract.
- **Representation confusion.** Returning HTML to a caller that asked for
`application/json` can cause client code to mis-handle the body (parsing an
HTML error page as JSON, or rendering untrusted HTML it never expected).
None of these is a critical vulnerability on its own, but together they are
concrete reasons the explicit-`Accept` approach is the safer default.
### Dead-code cleanup
While here, removed a sibling dead path — `if (msie) header = '*/*'` — whose
guard (`userAgent ==~ /msie(?i)/`) never matches a real Internet Explorer
`User-Agent` string and so never executed.
---
## Compatibility
- **Apps that declare `grails.mime.types`** are unaffected for their
declared types; previously-omitted types become additionally available via the
merge, and the first declared entry remains the default format.
- **Browser page loads** still resolve to HTML, because modern browsers rank
`text/html` highest — `withFormat { html { … } json { … } }` keeps serving HTML
to browsers and JSON to clients that ask for it.
- **To restore the Grails 7 behavior**, set
`grails.mime.disable.accept.header.userAgents: [Gecko, WebKit, Presto,
Trident]`.
- Upgrade notes added (sections 27 and 28 of the Grails 8 upgrade guide).
## Testing
- Full suites pass for `grails-mimetypes`, `grails-web-common`, and
`grails-test-suite-web`; Grails Forge view specs pass.
- New / updated specs cover: the full default MIME set, merge / override /
ordering of user config, the `Accept` header being honored for non-XHR browser
requests, and the opt-back-in config restoring the legacy behavior.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]