You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/02 20:36:29 UTC
(grails-core) branch 8.0.x updated (2d50fa049e -> 43b856c92a)
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a change to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
from 2d50fa049e Merge pull request #16458 from apache/fix/16455-default-datasource-bean
add b8950af8c9 Preserve servlet Errors through interceptor completion
add 6108b9f4ff Merge pull request #16467 from apache/fix/interceptor-error-throwables
add cffecea35f Link guide cross references to the chapter page that holds their target
add d8db2d173e Link cross references on sub-section and reference pages to their chapter
add 6a5bb12c0e Merge pull request #16476 from matrei/docs/guide-cross-chapter-links-7.0.x
add c8912e33f4 Merge branch '7.0.x' into 7.1.x
add b47e4dcb7a Merge branch '7.1.x' into 7.2.x
new 43b856c92a Merge branch '7.2.x' into 8.0.x
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../src/main/groovy/grails/doc/DocPublisher.groovy | 94 +++++++++--
.../test/groovy/grails/doc/DocPublisherSpec.groovy | 175 +++++++++++++++++++++
.../guide/backgroundJobs/backgroundJobsBasic.adoc | 2 +-
.../conventionOverConfiguration.adoc | 4 +-
.../springSecurity/acl/introduction.adoc | 2 +-
.../springSecurity/acl/tutorial.adoc | 2 +-
.../springSecurity/cas/introduction.adoc | 2 +-
.../securityPlugins/springSecurity/cas/usage.adoc | 2 +-
.../springSecurity/core/authentication/ajax.adoc | 2 +-
.../springSecurity/core/miscProperties.adoc | 4 +-
.../springSecurity/core/requestMappings.adoc | 2 +-
.../springSecurity/ldap/introduction.adoc | 2 +-
.../securityPlugins/springSecurity/ldap/usage.adoc | 2 +-
.../springSecurity/oauth2/introduction.adoc | 2 +-
.../springSecurity/rest/whatsNew20.adoc | 2 +-
.../securityPlugins/springSecurity/ui/acls.adoc | 2 +-
.../springSecurity/ui/introduction.adoc | 2 +-
.../interceptors/definingInterceptors.adoc | 5 +-
grails-doc/src/en/ref/Command Line/stop-app.adoc | 2 +-
grails-doc/src/en/ref/Plug-ins/codecs.adoc | 2 +-
grails-doc/src/en/ref/Tags - Fields/field.adoc | 2 +-
.../main/groovy/grails/artefact/Interceptor.groovy | 4 +-
...ailsInterceptorHandlerInterceptorAdapter.groovy | 12 +-
...InterceptorHandlerInterceptorAdapterSpec.groovy | 104 ++++++++++++
.../app1/grails-app/conf/spring/resources.groovy | 6 +
.../controllers/functionaltests/UrlMappings.groovy | 3 +
...terceptor.groovy => ErrorPageController.groovy} | 24 ++-
...erceptor.groovy => ErrorPageInterceptor.groovy} | 24 ++-
.../_simpleTemplate.gsp => errorPage/handle.gsp} | 5 +-
.../TomcatErrorPageInterceptorSpec.groovy | 76 +++++++++
.../interceptors/StaticInitFailure.groovy | 14 +-
.../interceptors/StaticInitFailureFilter.groovy | 22 +--
.../grails-app/conf/spring/resources.groovy | 9 +-
...ontroller.groovy => ErrorPageController.groovy} | 22 ++-
.../undertowapp/ErrorPageInterceptor.groovy} | 16 +-
.../controllers/undertowapp/UrlMappings.groovy | 1 +
.../grails-app/views/errorPage/handle.gsp} | 5 +-
.../UndertowErrorPageInterceptorSpec.groovy | 81 ++++++++++
.../groovy/undertowapp/StaticInitFailure.groovy} | 14 +-
.../undertowapp/StaticInitFailureFilter.groovy | 22 +--
40 files changed, 650 insertions(+), 128 deletions(-)
create mode 100644 build-logic/docs-core/src/test/groovy/grails/doc/DocPublisherSpec.groovy
copy grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/{HttpMethodMatchingInterceptor.groovy => ErrorPageController.groovy} (62%)
copy grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/{HttpMethodMatchingInterceptor.groovy => ErrorPageInterceptor.groovy} (69%)
copy grails-test-examples/app1/grails-app/views/{tagLibTest/_simpleTemplate.gsp => errorPage/handle.gsp} (89%)
create mode 100644 grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/TomcatErrorPageInterceptorSpec.groovy
copy grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/model/types/conversion/DoubleToFloatConverter.groovy => grails-test-examples/app1/src/main/groovy/functionaltests/interceptors/StaticInitFailure.groovy (72%)
copy grails-spring-security/rest/core/src/main/groovy/grails/plugin/springsecurity/rest/token/bearer/BearerTokenAccessDeniedHandler.groovy => grails-test-examples/app1/src/main/groovy/functionaltests/interceptors/StaticInitFailureFilter.groovy (58%)
copy grails-test-examples/undertow/grails-app/controllers/undertowapp/{SessionController.groovy => ErrorPageController.groovy} (60%)
copy grails-test-examples/{hibernate7/app1/grails-app/controllers/functionaltests/ErrorsControllerInterceptor.groovy => undertow/grails-app/controllers/undertowapp/ErrorPageInterceptor.groovy} (70%)
copy grails-test-examples/{app1/grails-app/views/tagLibTest/_simpleTemplate.gsp => undertow/grails-app/views/errorPage/handle.gsp} (89%)
create mode 100644 grails-test-examples/undertow/src/integration-test/groovy/undertowapp/UndertowErrorPageInterceptorSpec.groovy
copy grails-test-examples/undertow/{grails-app/init/undertowapp/Application.groovy => src/main/groovy/undertowapp/StaticInitFailure.groovy} (74%)
copy grails-spring-security/rest/core/src/main/groovy/grails/plugin/springsecurity/rest/token/bearer/BearerTokenAccessDeniedHandler.groovy => grails-test-examples/undertow/src/main/groovy/undertowapp/StaticInitFailureFilter.groovy (58%)
(grails-core) 01/01: Merge branch '7.2.x' into 8.0.x
Posted by jd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 43b856c92a5b30b7bf3f197be6343bd88dbb62b7
Merge: 2d50fa049e b47e4dcb7a
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Oct 2 16:36:20 2026 -0400
Merge branch '7.2.x' into 8.0.x
# Conflicts:
# build-logic/docs-core/build.gradle
# grails-doc/src/en/guide/security/securityPlugins/springSecurity/cas/introduction.adoc
# grails-interceptors/src/test/groovy/grails/artefact/GrailsInterceptorHandlerInterceptorAdapterSpec.groovy
# grails-test-examples/app1/grails-app/conf/spring/resources.groovy
# grails-test-examples/undertow/build.gradle
# grails-test-examples/undertow/grails-app/conf/application.yml
# grails-test-examples/undertow/grails-app/conf/spring/resources.groovy
# grails-test-examples/undertow/grails-app/controllers/undertowapp/UrlMappings.groovy
# settings.gradle
.../src/main/groovy/grails/doc/DocPublisher.groovy | 94 +++++++++--
.../test/groovy/grails/doc/DocPublisherSpec.groovy | 175 +++++++++++++++++++++
.../guide/backgroundJobs/backgroundJobsBasic.adoc | 2 +-
.../conventionOverConfiguration.adoc | 4 +-
.../springSecurity/acl/introduction.adoc | 2 +-
.../springSecurity/acl/tutorial.adoc | 2 +-
.../springSecurity/cas/introduction.adoc | 2 +-
.../securityPlugins/springSecurity/cas/usage.adoc | 2 +-
.../springSecurity/core/authentication/ajax.adoc | 2 +-
.../springSecurity/core/miscProperties.adoc | 4 +-
.../springSecurity/core/requestMappings.adoc | 2 +-
.../springSecurity/ldap/introduction.adoc | 2 +-
.../securityPlugins/springSecurity/ldap/usage.adoc | 2 +-
.../springSecurity/oauth2/introduction.adoc | 2 +-
.../springSecurity/rest/whatsNew20.adoc | 2 +-
.../securityPlugins/springSecurity/ui/acls.adoc | 2 +-
.../springSecurity/ui/introduction.adoc | 2 +-
.../interceptors/definingInterceptors.adoc | 5 +-
grails-doc/src/en/ref/Command Line/stop-app.adoc | 2 +-
grails-doc/src/en/ref/Plug-ins/codecs.adoc | 2 +-
grails-doc/src/en/ref/Tags - Fields/field.adoc | 2 +-
.../main/groovy/grails/artefact/Interceptor.groovy | 4 +-
...ailsInterceptorHandlerInterceptorAdapter.groovy | 12 +-
...InterceptorHandlerInterceptorAdapterSpec.groovy | 104 ++++++++++++
.../app1/grails-app/conf/spring/resources.groovy | 6 +
.../controllers/functionaltests/UrlMappings.groovy | 3 +
.../interceptors/ErrorPageController.groovy} | 23 +--
.../interceptors/ErrorPageInterceptor.groovy} | 23 +--
.../app1/grails-app/views/errorPage/handle.gsp | 18 +++
.../TomcatErrorPageInterceptorSpec.groovy | 76 +++++++++
.../interceptors/StaticInitFailure.groovy} | 22 ++-
.../interceptors/StaticInitFailureFilter.groovy | 41 +++++
.../grails-app/conf/spring/resources.groovy | 9 +-
...lMappings.groovy => ErrorPageController.groovy} | 23 +--
...Mappings.groovy => ErrorPageInterceptor.groovy} | 23 +--
.../controllers/undertowapp/UrlMappings.groovy | 1 +
.../undertow/grails-app/views/errorPage/handle.gsp | 18 +++
.../UndertowErrorPageInterceptorSpec.groovy | 81 ++++++++++
.../groovy/undertowapp/StaticInitFailure.groovy} | 20 ++-
.../undertowapp/StaticInitFailureFilter.groovy} | 28 ++--
40 files changed, 734 insertions(+), 115 deletions(-)
diff --cc grails-doc/src/en/guide/security/securityPlugins/springSecurity/cas/introduction.adoc
index 90b4cb0c5e,c658cf18e5..944fddb226
--- a/grails-doc/src/en/guide/security/securityPlugins/springSecurity/cas/introduction.adoc
+++ b/grails-doc/src/en/guide/security/securityPlugins/springSecurity/cas/introduction.adoc
@@@ -20,6 -20,6 +20,6 @@@ under the License
[[cas-introduction]]
== Introduction to the Spring Security CAS Plugin
- The CAS plugin adds https://apereo.github.io/cas[CAS] single sign-on support to a Grails application that uses Spring Security. It depends on the xref:springSecurityCore.adoc#springSecurityCore[Spring Security Core plugin].
-The CAS plugin adds https://www.jasig.org/cas[CAS] single sign-on support to a Grails application that uses Spring Security. It depends on the <<springSecurityCore,Spring Security Core plugin>>.
++The CAS plugin adds https://apereo.github.io/cas[CAS] single sign-on support to a Grails application that uses Spring Security. It depends on the <<springSecurityCore,Spring Security Core plugin>>.
Once you have configured a CAS server and have configured your Grails application(s) as clients, you can authenticate to any application that is a client of the CAS server and be automatically authenticated to all other clients.
diff --cc grails-interceptors/src/main/groovy/org/grails/plugins/web/interceptors/GrailsInterceptorHandlerInterceptorAdapter.groovy
index 58f1411406,b6c266a52d..78ca2154ff
--- a/grails-interceptors/src/main/groovy/org/grails/plugins/web/interceptors/GrailsInterceptorHandlerInterceptorAdapter.groovy
+++ b/grails-interceptors/src/main/groovy/org/grails/plugins/web/interceptors/GrailsInterceptorHandlerInterceptorAdapter.groovy
@@@ -136,14 -117,18 +136,18 @@@ class GrailsInterceptorHandlerIntercept
@Override
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
- if (!ex) {
- //Attempting to find an existing exception in the request
- ex = (Exception) request.getAttribute(WebUtils.EXCEPTION_ATTRIBUTE)
+ Throwable throwable = ex
+ if (throwable == null) {
+ // The request attribute may hold an Error, or a value that is not a Throwable at all
+ Object requestException = request.getAttribute(WebUtils.EXCEPTION_ATTRIBUTE)
+ if (requestException instanceof Throwable) {
+ throwable = (Throwable) requestException
+ }
}
- request.setAttribute(Matcher.THROWABLE, ex)
+ request.setAttribute(Matcher.THROWABLE, throwable)
Object matchedInterceptorsObject = request.getAttribute(ATTRIBUTE_MATCHED_INTERCEPTORS)
if (matchedInterceptorsObject) {
- for (i in ((List<Interceptor>) matchedInterceptorsObject)) {
+ for (Interceptor i in ((List<Interceptor>) matchedInterceptorsObject)) {
i.afterView()
}
}
diff --cc grails-interceptors/src/test/groovy/grails/artefact/GrailsInterceptorHandlerInterceptorAdapterSpec.groovy
index 9376ec1921,3a81634104..c470d6bffe
--- a/grails-interceptors/src/test/groovy/grails/artefact/GrailsInterceptorHandlerInterceptorAdapterSpec.groovy
+++ b/grails-interceptors/src/test/groovy/grails/artefact/GrailsInterceptorHandlerInterceptorAdapterSpec.groovy
@@@ -20,11 -20,9 +20,12 @@@ package grails.artefac
import grails.interceptors.Matcher
import grails.util.GrailsWebMockUtil
+import io.micrometer.observation.Observation
+import io.micrometer.observation.ObservationHandler
+import io.micrometer.observation.ObservationRegistry
import org.grails.plugins.web.interceptors.GrailsInterceptorHandlerInterceptorAdapter
import org.grails.web.servlet.mvc.GrailsWebRequest
+ import org.grails.web.util.WebUtils
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.servlet.ModelAndView
import spock.lang.Issue
@@@ -152,305 -127,109 +153,408 @@@ class GrailsInterceptorHandlerIntercept
webRequest.request.getAttribute(Matcher.THROWABLE) instanceof Exception
}
+ void 'afterView receives the original request #description'() {
+ given:
+ def interceptor = new CompletionRecordingInterceptor()
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([interceptor] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ webRequest.request.setAttribute(WebUtils.EXCEPTION_ATTRIBUTE, failure)
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ when:
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, null)
+
+ then:
+ interceptor.completions == 1
+ interceptor.observedThrowable.is(failure)
+ webRequest.request.getAttribute(Matcher.THROWABLE).is(failure)
+
+ where:
+ description | failure
+ 'checked exception' | new Exception('checked')
+ 'runtime exception' | new IllegalStateException('runtime')
+ 'initialization error' | new ExceptionInInitializerError(new IllegalStateException('initializer'))
+ 'assertion error' | new AssertionError('assertion')
+ 'throwable' | new Throwable('failure')
+ 'without a failure' | null
+ }
+
+ void 'afterView ignores a request exception attribute holding #description'() {
+ given: 'a value that is not a Throwable stored under the exception request attribute'
+ def interceptor = new CompletionRecordingInterceptor()
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([interceptor] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ webRequest.request.setAttribute(WebUtils.EXCEPTION_ATTRIBUTE, value)
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ when:
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, null)
+
+ then: 'afterView still runs and sees no throwable'
+ interceptor.completions == 1
+ interceptor.observedThrowable == null
+ webRequest.request.getAttribute(Matcher.THROWABLE) == null
+
+ where:
+ description | value
+ 'a String' | 'Something went wrong'
+ 'a Map' | [message: 'Something went wrong']
+ }
+
+ void 'every matched interceptor receives a request error in afterView'() {
+ given:
+ def first = new CompletionRecordingInterceptor()
+ def second = new CompletionRecordingInterceptor()
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([first, second] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ def failure = new ExceptionInInitializerError(new IllegalStateException('initializer'))
+ webRequest.request.setAttribute(WebUtils.EXCEPTION_ATTRIBUTE, failure)
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ when:
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, null)
+
+ then:
+ [first, second].every { it.completions == 1 && it.observedThrowable.is(failure) }
+ }
+
+ void 'an explicit completion exception takes precedence over a request error'() {
+ given:
+ def interceptor = new CompletionRecordingInterceptor()
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([interceptor] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ webRequest.request.setAttribute(WebUtils.EXCEPTION_ATTRIBUTE, new AssertionError('request error'))
+ def failure = new IllegalStateException('completion exception')
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ when:
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, failure)
+
+ then:
+ interceptor.completions == 1
+ interceptor.observedThrowable.is(failure)
+ }
++
+ void "Test observation is disabled by default and the registry is left untouched"() {
+ given: "An adapter with the default observation registry"
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([new HighestInterceptor(), new LowestInterceptor()] as Interceptor[])
+
+ expect: "The default registry is a no-op"
+ adapter.observationRegistry.isNoop()
+
+ when: "A request is handled"
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ def modelAndView = new ModelAndView()
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ then: "The interceptors still run in order"
+ webRequest.request.getAttribute('executed') == ['highest before', 'lowest before']
+
+ when: "The remaining phases run"
+ webRequest.request.setAttribute('executed', null)
+ adapter.postHandle(webRequest.request, webRequest.response, this, modelAndView)
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, null)
+
+ then: "They still run in reverse order"
+ webRequest.request.getAttribute('executed') == ['lowest after', 'highest after', 'lowest afterView', 'highest afterView']
+ }
+
+ void "Test a no-op registry is never asked to create an observation"() {
+ given: "An adapter wired to a registry that reports itself as a no-op"
+ def registry = Mock(ObservationRegistry)
+ registry.isNoop() >> true
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.observationRegistry = registry
+ adapter.setInterceptors([new MyInterceptor()] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+
+ when: "A request is handled"
+ def proceed = adapter.preHandle(webRequest.request, webRequest.response, this)
+ adapter.postHandle(webRequest.request, webRequest.response, this, new ModelAndView())
+
+ then: "The interceptor result is honoured"
+ proceed
+
+ and: "No observation state is created from the registry"
+ 0 * registry.observationConfig()
+ 0 * registry.getCurrentObservationScope()
+ }
+
+ void "Test a null observation registry falls back to invoking the interceptor directly"() {
+ given: "An adapter with no observation registry at all"
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.observationRegistry = null
+ adapter.setInterceptors([new MyInterceptor()] as Interceptor[])
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ def modelAndView = new ModelAndView()
+
+ when: "A request is handled"
+ def proceed = adapter.preHandle(webRequest.request, webRequest.response, this)
+ adapter.postHandle(webRequest.request, webRequest.response, this, modelAndView)
+
+ then: "The interceptor callbacks still drive the result"
+ proceed
+ modelAndView.model.foo == 'bar'
+ modelAndView.viewName == 'foo'
+ }
+
+ void "Test an observation is recorded for each interceptor phase when observation is enabled"() {
+ given: "An adapter wired to a registry with a recording handler"
+ def handler = new RecordingObservationHandler()
+ def adapter = observingAdapter(handler, new HighestInterceptor(), new LowestInterceptor())
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ def modelAndView = new ModelAndView()
+
+ when: "The before phase runs"
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ then: "One observation per matched interceptor is recorded, in execution order"
+ handler.started.size() == 2
+ handler.stopped.size() == 2
+ handler.errored.isEmpty()
+ handler.scopesOpened == 2
+ handler.scopesClosed == 2
+
+ and: "Each observation carries the interceptor name and phase"
+ handler.started.every { it.name == 'grails.interceptor' }
+ handler.names == ['highest', 'lowest']
+ handler.phases == ['before', 'before']
+ handler.contextualNames == ['grails.interceptor highest', 'grails.interceptor lowest']
+
+ when: "The after phase runs"
+ handler.reset()
+ adapter.postHandle(webRequest.request, webRequest.response, this, modelAndView)
+
+ then: "Observations are recorded in reverse order and tagged as the after phase"
+ handler.started.size() == 2
+ handler.names == ['lowest', 'highest']
+ handler.phases == ['after', 'after']
+ handler.contextualNames == ['grails.interceptor lowest', 'grails.interceptor highest']
+
+ when: "The afterView phase runs"
+ handler.reset()
+ adapter.afterCompletion(webRequest.request, webRequest.response, this, null)
+
+ then: "It is not observed"
+ handler.started.isEmpty()
+ }
+
+ void "Test an observed interceptor that vetoes the request still cancels processing"() {
+ given: "An observed interceptor that vetoes"
+ def handler = new RecordingObservationHandler()
+ def adapter = observingAdapter(handler, new MyInterceptor())
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ webRequest.request.setAttribute('something', 'test')
+
+ when: "The before phase runs"
+ def proceed = adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ then: "Processing is cancelled and the observation is still completed cleanly"
+ !proceed
+ handler.started.size() == 1
+ handler.stopped.size() == 1
+ handler.errored.isEmpty()
+ handler.scopesClosed == 1
+ }
+
+ void "Test an observed interceptor that vetoes view rendering still clears the model and view"() {
+ given: "An observed interceptor that vetoes the after phase"
+ def handler = new RecordingObservationHandler()
+ def adapter = observingAdapter(handler, new MyInterceptor())
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+ def modelAndView = new ModelAndView()
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+ adapter.postHandle(webRequest.request, webRequest.response, this, modelAndView)
+
+ when: "A condition is met for exclusion"
+ webRequest.request.setAttribute('bar', 'test')
+ adapter.postHandle(webRequest.request, webRequest.response, this, modelAndView)
+
+ then: "The view is cleared"
+ modelAndView.viewName == null
+
+ and: "Every observation was stopped"
+ handler.started.size() == handler.stopped.size()
+ handler.errored.isEmpty()
+ }
+
+ void "Test an exception thrown by an observed interceptor is recorded and rethrown"() {
+ given: "An observed interceptor that throws"
+ def handler = new RecordingObservationHandler()
+ def failure = new IllegalStateException('boom')
+ def adapter = observingAdapter(handler, new ExplodingInterceptor(failure: failure))
+ def webRequest = GrailsWebMockUtil.bindMockWebRequest()
+
+ when: "The before phase runs"
+ adapter.preHandle(webRequest.request, webRequest.response, this)
+
+ then: "The original exception propagates"
+ def e = thrown(IllegalStateException)
+ e.is(failure)
+
+ and: "The observation recorded the error and was still stopped"
+ handler.errored.size() == 1
+ handler.errored.first().error.is(failure)
+ handler.stopped.size() == 1
+ handler.scopesOpened == 1
+ handler.scopesClosed == 1
+ }
+
+ void "Test interceptor names are resolved per interceptor class across repeated requests"() {
+ given: "An adapter observing two different interceptor classes"
+ def handler = new RecordingObservationHandler()
+ def adapter = observingAdapter(handler, new HighestInterceptor(), new LowestInterceptor())
+
+ when: "Two requests are handled by the same adapter"
+ def firstRequest = GrailsWebMockUtil.bindMockWebRequest()
+ adapter.preHandle(firstRequest.request, firstRequest.response, this)
+ def firstNames = handler.names
+
+ handler.reset()
+ RequestContextHolder.setRequestAttributes(null)
+ def secondRequest = GrailsWebMockUtil.bindMockWebRequest()
+ adapter.preHandle(secondRequest.request, secondRequest.response, this)
+
+ then: "The cached names are per class, not shared between classes or requests"
+ firstNames == ['highest', 'lowest']
+ handler.names == ['highest', 'lowest']
+ }
+
+ void "Test the matched interceptor list is not carried over between requests"() {
+ given: "An adapter reused across requests"
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.setInterceptors([new HighestInterceptor(), new LowestInterceptor()] as Interceptor[])
+
+ when: "A first request runs through every phase"
+ def firstRequest = GrailsWebMockUtil.bindMockWebRequest()
+ adapter.preHandle(firstRequest.request, firstRequest.response, this)
+ firstRequest.request.setAttribute('executed', null)
+ adapter.postHandle(firstRequest.request, firstRequest.response, this, new ModelAndView())
+ adapter.afterCompletion(firstRequest.request, firstRequest.response, this, null)
+
+ then: "The order is reversed exactly once"
+ firstRequest.request.getAttribute('executed') == ['lowest after', 'highest after', 'lowest afterView', 'highest afterView']
+
+ when: "A second request runs through the same adapter"
+ RequestContextHolder.setRequestAttributes(null)
+ def secondRequest = GrailsWebMockUtil.bindMockWebRequest()
+ adapter.preHandle(secondRequest.request, secondRequest.response, this)
+
+ then: "The before phase still runs in the original order"
+ secondRequest.request.getAttribute('executed') == ['highest before', 'lowest before']
+
+ when: "The remaining phases run"
+ secondRequest.request.setAttribute('executed', null)
+ adapter.postHandle(secondRequest.request, secondRequest.response, this, new ModelAndView())
+ adapter.afterCompletion(secondRequest.request, secondRequest.response, this, null)
+
+ then: "The order is reversed again, independently of the first request"
+ secondRequest.request.getAttribute('executed') == ['lowest after', 'highest after', 'lowest afterView', 'highest afterView']
+ }
+
+ private static GrailsInterceptorHandlerInterceptorAdapter observingAdapter(RecordingObservationHandler handler,
+ Interceptor... interceptors) {
+ def registry = ObservationRegistry.create()
+ registry.observationConfig().observationHandler(handler)
+ def adapter = new GrailsInterceptorHandlerInterceptorAdapter()
+ adapter.observationRegistry = registry
+ adapter.setInterceptors(interceptors)
+ adapter
+ }
+}
+class RecordingObservationHandler implements ObservationHandler<Observation.Context> {
+
+ final List<Observation.Context> started = []
+ final List<Observation.Context> stopped = []
+ final List<Observation.Context> errored = []
+ int scopesOpened
+ int scopesClosed
+
+ @Override
+ void onStart(Observation.Context context) {
+ started << context
+ }
+
+ @Override
+ void onStop(Observation.Context context) {
+ stopped << context
+ }
+
+ @Override
+ void onError(Observation.Context context) {
+ errored << context
+ }
+
+ @Override
+ void onScopeOpened(Observation.Context context) {
+ scopesOpened++
+ }
+
+ @Override
+ void onScopeClosed(Observation.Context context) {
+ scopesClosed++
+ }
+
+ @Override
+ boolean supportsContext(Observation.Context context) {
+ true
+ }
+
+ List<String> getNames() {
+ started.collect { it.getLowCardinalityKeyValue('grails.interceptor')?.value }
+ }
+
+ List<String> getPhases() {
+ started.collect { it.getLowCardinalityKeyValue('grails.interceptor.phase')?.value }
+ }
+
+ List<String> getContextualNames() {
+ started.collect { it.contextualName }
+ }
+
+ void reset() {
+ started.clear()
+ stopped.clear()
+ errored.clear()
+ scopesOpened = 0
+ scopesClosed = 0
+ }
+}
+class ExplodingInterceptor implements Interceptor {
+
+ RuntimeException failure
+
+ ExplodingInterceptor() {
+ matchAll()
+ }
+
+ @Override
+ boolean before() {
+ throw failure
+ }
}
+
+ class CompletionRecordingInterceptor implements Interceptor {
+
+ Throwable observedThrowable
+ int completions
+
+ CompletionRecordingInterceptor() {
+ matchAll()
+ }
+
+ @Override
+ void afterView() {
+ observedThrowable = throwable
+ completions++
+ }
+ }
+
class MyInterceptor implements Interceptor {
MyInterceptor() {
diff --cc grails-test-examples/app1/grails-app/conf/spring/resources.groovy
index 157fa761c6,9d2334d7ee..b750f3fad0
--- a/grails-test-examples/app1/grails-app/conf/spring/resources.groovy
+++ b/grails-test-examples/app1/grails-app/conf/spring/resources.groovy
@@@ -17,8 -17,21 +17,14 @@@
* under the License.
*/
+ import functionaltests.interceptors.StaticInitFailureFilter
import org.example.MyBean
-import org.grails.spring.context.support.PluginAwareResourceBundleMessageSource
+ import org.springframework.boot.web.servlet.FilterRegistrationBean
// Place your Spring DSL code here
beans = {
myBean(MyBean)
- // Explicit basenames must be resolved ahead of the bundles discovered in grails-app/i18n (GH #11795)
- messageSource(PluginAwareResourceBundleMessageSource) {
- fallbackToSystemLocale = false
- defaultEncoding = 'UTF-8'
- basenames = ['overrides', 'i18n-extra/external', 'messages']
- }
+ staticInitFailureFilter(FilterRegistrationBean) {
+ filter = new StaticInitFailureFilter()
+ urlPatterns = ['/staticInitFailure/*']
+ }
}
diff --cc grails-test-examples/undertow/grails-app/controllers/undertowapp/UrlMappings.groovy
index 25ea46d896,e19e3a100b..9b2377aea3
--- a/grails-test-examples/undertow/grails-app/controllers/undertowapp/UrlMappings.groovy
+++ b/grails-test-examples/undertow/grails-app/controllers/undertowapp/UrlMappings.groovy
@@@ -27,9 -27,8 +27,10 @@@ class UrlMappings
}
}
+ "/"(view: "/index")
"500"(view: '/error')
+ "500"(controller: 'errorPage', action: 'handle', exception: StaticInitFailure)
+ "413"(controller: 'upload', action: 'tooLarge')
"404"(view: '/notFound')
}
}