GrailsExceptionResolver masks original exception with GrailsWebRequest cast failure
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
## Description
`GrailsExceptionResolver` can mask the original application exception when Spring's `RequestContextHolder` contains a plain `ServletRequestAttributes` rather than a `GrailsWebRequest`.
During status-code mapping, `AbstractUrlMappingInfo.evaluateNameForValue` unconditionally casts the current request attributes to `GrailsWebRequest`. The resulting `ClassCastException` replaces the original exception and does not preserve it as a cause.
## Reproduction
Using the official Grails `8.0.0-M5` distribution, bind a plain `ServletRequestAttributes` and call the public exception resolver with an explicit application exception:
```groovy
RequestContextHolder.setRequestAttributes(
new ServletRequestAttributes(new MockHttpServletRequest(), new MockHttpServletResponse())
)
def original = new IllegalStateException('ORIGINAL_EXCEPTION')
exceptionResolver.resolveException(request, response, null, original)
```
A full Spring `MockMvc` dispatch also reproduces the condition because MockMvc binds `ServletRequestAttributes`. Grails encounters an earlier handler-lookup error, then replaces that error with the same cast failure while resolving it.
## Actual behavior
```text
java.lang.ClassCastException: class org.springframework.web.context.request.ServletRequestAttributes cannot be cast to class org.grails.web.servlet.mvc.GrailsWebRequest
at org.grails.web.mapping.AbstractUrlMappingInfo.evaluateNameForValue(AbstractUrlMappingInfo.java:124)
at org.grails.web.mapping.DefaultUrlMappingInfo.getNamespace(DefaultUrlMappingInfo.java:189)
at org.grails.web.mapping.mvc.AbstractGrailsControllerUrlMappings.collectControllerMapping(AbstractGrailsControllerUrlMappings.groovy:267)
at org.grails.web.mapping.mvc.AbstractGrailsControllerUrlMappings.matchStatusCode(AbstractGrailsControllerUrlMappings.groovy:145)
at org.grails.web.errors.GrailsExceptionResolver.matchStatusCode(GrailsExceptionResolver.java:267)
at org.grails.web.errors.GrailsExceptionResolver.resolveViewOrForward(GrailsExceptionResolver.java:196)
at org.grails.web.errors.GrailsExceptionResolver.resolveException(GrailsExceptionResolver.java:113)
```
The `ClassCastException` has no cause, so the original `IllegalStateException("ORIGINAL_EXCEPTION")` is lost.
## Expected behavior
Exception resolution should not assume every `RequestAttributes` implementation is a `GrailsWebRequest`. It should preserve and resolve the original exception, or safely treat Grails-specific URL mapping values as unavailable when the current attributes are not a `GrailsWebRequest`.
## Environment
- Grails: `8.0.0-M5`
- Groovy: `5.0.8`
- Spring Framework: `7.0.8`
- Spring Boot: `4.1.0`
- Java: `21.0.11`
The isolated application build passed with both reproduction tests, including the full MockMvc path and direct public resolver path.
Contributor guide
Research direction
Start at AbstractUrlMappingInfo.evaluateNameForValue and follow its use from GrailsExceptionResolver.matchStatusCode through the URL-mapping classes. Reproduce the failure with plain ServletRequestAttributes using the direct resolver or full MockMvc path, then verify that exception resolution preserves the original application exception when GrailsWebRequest is unavailable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy, spring
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100