apache / apache/grails-core

ResponseRenderer: non-view render() overloads should call flushBuffer() like render(status:) already does

Open
#15,819 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.