ResponseRenderer: non-view render() overloads should call flushBuffer() like render(status:) already does
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
## Expected Behavior
Calling `render(template:)`, `render(text:)`, `render(... as JSON)` etc. in a controller action commits the response, preventing Spring MVC from attempting default view resolution.
## Actual Behaviour
The response is left uncommitted after the action returns. `DispatcherServlet.processDispatchResult` sees `isCommitted() == false`, falls through to view resolution, and throws:
```
jakarta.servlet.ServletException: Could not resolve view with name ''
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1410)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1167)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1105)
```
The bug is invisible in unit tests (`MockHttpServletResponse` has no view-resolver fallthrough) and only surfaces at runtime on AJAX endpoints that return template/text/JSON fragments.
**Root cause:** `ResponseRenderer.groovy` already calls `response.flushBuffer()` in its `render(status:)` branch. The `ARGUMENT_TEMPLATE`, `ARGUMENT_TEXT`, JSON-object, writable, closure, and `Object` branches all set `webRequest.renderView = false` — Grails' own signal that no view resolution is needed — but nothing translates that flag into `response.isCommitted() == true` for `DispatcherServlet`.
## Steps To Reproduce
1. Create a Grails 7 controller with an action that calls `render(template: '_results', model: [...])` on an AJAX endpoint with no GSP layout
2. Hit the action from a browser
3. Observe `Could not resolve view with name ''`
4. Workaround: add `response.flushBuffer()` after the `render(...)` call
## Relationship to #14193
Issue #14193 described the same `DispatcherServlet` fallthrough in a forwarding scenario and was fixed by reverting to SiteMesh 2 (PR #14875). That fix addressed the forward-through-layout path. The direct `render(template:)` / `render(text:)` path is **not** covered — SiteMesh is not involved for partial/AJAX renders with no layout, so the revert did not fix this variant.
## Proposed Fix
Add `response.flushBuffer()` at the end of each branch in `ResponseRenderer.groovy` that sets `webRequest.renderView = false` and writes content to the response. The existing `render(status:)` branch already does this correctly and can serve as the pattern. One-line fix per affected branch.
## Workaround
We carry a compile-time `TraitInjector` in our application that injects a Groovy trait wrapping every public `render(...)` overload to call `response.flushBuffer()` after `super.render(...)` when `GrailsWebRequest.isRenderView()` is `false`. Happy to share the implementation if useful as a reference for the fix.
## Environment Information
- Operating System: any
- JDK Version: 17+
- Grails Version: 7.1.1
- Spring Boot 3.x / Spring MVC 6.x / Tomcat (Coyote)
## Version
7.1.1
Contributor guide
Research direction
Start in ResponseRenderer.groovy by comparing the existing render(status:) branch with the ARGUMENT_TEMPLATE, ARGUMENT_TEXT, JSON-object, writable, closure, and Object branches. Reproduce the controller render path and inspect DispatcherServlet.processDispatchResult to verify the response is committed and view resolution is not attempted. Done means template, text, and JSON fragment renders no longer fail at runtime without a manual flushBuffer() call.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100