Skip to content

GrailsExceptionResolver masks original exception with GrailsWebRequest cast failure #16129

Description

@jamesfredley

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions