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:
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
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.
Description
GrailsExceptionResolvercan mask the original application exception when Spring'sRequestContextHoldercontains a plainServletRequestAttributesrather than aGrailsWebRequest.During status-code mapping,
AbstractUrlMappingInfo.evaluateNameForValueunconditionally casts the current request attributes toGrailsWebRequest. The resultingClassCastExceptionreplaces the original exception and does not preserve it as a cause.Reproduction
Using the official Grails
8.0.0-M5distribution, bind a plainServletRequestAttributesand call the public exception resolver with an explicit application exception:A full Spring
MockMvcdispatch also reproduces the condition because MockMvc bindsServletRequestAttributes. Grails encounters an earlier handler-lookup error, then replaces that error with the same cast failure while resolving it.Actual behavior
The
ClassCastExceptionhas no cause, so the originalIllegalStateException("ORIGINAL_EXCEPTION")is lost.Expected behavior
Exception resolution should not assume every
RequestAttributesimplementation is aGrailsWebRequest. It should preserve and resolve the original exception, or safely treat Grails-specific URL mapping values as unavailable when the current attributes are not aGrailsWebRequest.Environment
8.0.0-M55.0.87.0.84.1.021.0.11The isolated application build passed with both reproduction tests, including the full MockMvc path and direct public resolver path.