apache / apache/grails-core

GrailsExceptionResolver masks original exception with GrailsWebRequest cast failure

Open
#16,129 0 comments 0 reactions 0 assignees View on GitHub
type: bug
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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.