Skip to content

Fix rendering outside a web request in non TEST env (#60) - #61

Merged
sbglasius merged 6 commits into
7.0.xfrom
fix/60-render-outside-web-request
Oct 7, 2026
Merged

sbglasius merged 6 commits into
7.0.xfrom
fix/60-render-outside-web-request

Conversation

@billgonemad

Copy link
Copy Markdown
Collaborator

Fixes #60

Problem

Since 364bd4e, RenderEnvironment only bound a web request in the TEST environment. In production/development, rendering off-request (jobs, async tasks, BootStrap) NPE'd as soon as a template used a taglib.

Fix

  • No request bound: binds a GrailsWebRequest over Grails' servlet-free PageRenderer stubs (the same ones groovyPageRenderer uses). It uses the real context path and runs request-scoped cleanup.
  • Request already bound: reuses the caller's request, so session, flash, params and request attributes keep working. Only out, the locale resolver and WrappedResponseHolder are swapped, and they are restored afterwards.
  • The caller's request state is always restored, including on exceptions and nested renders.

Behaviour change

Off-request, session, without grails.serverURLthrow UnsupportedOperationExceptionin all environments. They already NPE'd in production on 7.0.x, but in TEST they used to work, so downstream test suites may notice. Setgrails.serverURL`.

Testing

New integration specs force grails.env=production, since TEST hid the bug. 14 of the 18 new tests fail against 7.0.x. ./gradlew check build docs :example:integrationTest: 90 tests, 0 failures.

…#60)

RenderEnvironment only bound a request in the TEST environment and relied on spring-test mocks, so rendering from jobs/async tasks in production NPE'd in taglibs. Use PageRenderer's request/response proxies in every environment.
@sbglasius

Copy link
Copy Markdown
Member

Minor suggestion on RenderEnvironment.with(...): env.init() runs outside the try/finally. init() binds the render request via RequestContextHolder.setRequestAttributes(...) and then sets request attributes and WrappedResponseHolder. If a later step throws, close() never runs, so the mock request stays bound on the thread and the caller state is not restored. That matters on pooled job threads.

Suggestion: make init() safe on partial failure. For example, wrap the body after the binding in a try/catch that calls restoreCallerState() and rethrows, or move env.init() inside the try. If you do the latter, make close() tolerate renderRequestAttributes == null, because init() can fail before it is assigned.

@billgonemad

Copy link
Copy Markdown
Collaborator Author

@sbglasius Thanks for the review. Went with moving env.init() inside of the try block and updating the close()

@sbglasius sbglasius left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-reviewed 1462b37: init() failure is now handled (init inside try, close() tolerates unset render request). LGTM.

@sbglasius
sbglasius merged commit e14077c into 7.0.x Oct 7, 2026
5 checks passed
@sbglasius
sbglasius deleted the fix/60-render-outside-web-request branch October 7, 2026 10:52
@sbglasius

Copy link
Copy Markdown
Member

@billgonemad Thank you for your contribution. The release is running and the artifact should be available as version 7.0.2 in the next hour or so.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rendering outside of web requests not working anymore

2 participants