This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch 7.1.x in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 796d6b3d550e102793322c129851396f70513c4c Merge: c964b9328f 642bcd5006 Author: James Daugherty <[email protected]> AuthorDate: Thu Jul 9 22:42:11 2026 -0400 Merge branch '7.0.x' into 7.1.x # Conflicts: # grails-web-mvc/src/test/groovy/org/grails/web/errors/GrailsExceptionResolverSpec.groovy ...maskingRequestParametersFromStacktraceLogs.adoc | 1 + .../META-INF/spring-configuration-metadata.json | 5 +- .../grails/web/errors/GrailsExceptionResolver.java | 14 +- .../web/errors/GrailsExceptionResolverSpec.groovy | 348 ++++++++++++--------- 4 files changed, 208 insertions(+), 160 deletions(-) diff --cc grails-web-mvc/src/test/groovy/org/grails/web/errors/GrailsExceptionResolverSpec.groovy index 384c382a7a,707c2e74fc..15ec0d61c3 --- a/grails-web-mvc/src/test/groovy/org/grails/web/errors/GrailsExceptionResolverSpec.groovy +++ b/grails-web-mvc/src/test/groovy/org/grails/web/errors/GrailsExceptionResolverSpec.groovy @@@ -46,262 -45,35 +46,298 @@@ class GrailsExceptionResolverSpec exten noExceptionThrown() params.isEmpty() } - void "getRequestLogMessage masks excluded request parameters case-insensitively"() { + + void "logStackTrace emits only the resolver log"() { + given: "Captured System.err" - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) ++ def originalErr = System.err ++ def baos = new ByteArrayOutputStream() ++ System.setErr(new PrintStream(baos, true)) + + and: "A resolver with no grailsApplication wired" - def resolver = new GrailsExceptionResolver() - def request = new MockHttpServletRequest('GET', '/test') - def exception = new RuntimeException('boom') ++ def resolver = new GrailsExceptionResolver() ++ def request = new MockHttpServletRequest('GET', '/test') ++ def exception = new RuntimeException('boom') + + when: - resolver.logStackTrace(exception, request) ++ resolver.logStackTrace(exception, request) + + then: "Only the GrailsExceptionResolver logger emits; StackTrace logger is silent" - System.err.flush() - def captured = baos.toString() - captured.contains('o.g.web.errors.GrailsExceptionResolver') || ++ System.err.flush() ++ def captured = baos.toString() ++ captured.contains('o.g.web.errors.GrailsExceptionResolver') || + captured.contains('org.grails.web.errors.GrailsExceptionResolver') - !captured.contains('ERROR StackTrace ') ++ !captured.contains('ERROR StackTrace ') + + cleanup: - System.setErr(originalErr) ++ System.setErr(originalErr) + } + + void "logFullStackTraceIfEnabled is a no-op when the opt-in property is unset"() { + given: "Captured System.err" - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) ++ def originalErr = System.err ++ def baos = new ByteArrayOutputStream() ++ System.setErr(new PrintStream(baos, true)) + + and: "A resolver with no grailsApplication wired" - def resolver = new GrailsExceptionResolver() - def exception = new RuntimeException('boom') ++ def resolver = new GrailsExceptionResolver() ++ def exception = new RuntimeException('boom') + + when: - resolver.logFullStackTraceIfEnabled(exception) ++ resolver.logFullStackTraceIfEnabled(exception) + + then: "No StackTrace log entry is emitted" - System.err.flush() - !baos.toString().contains('ERROR StackTrace ') ++ System.err.flush() ++ !baos.toString().contains('ERROR StackTrace ') + + cleanup: - System.setErr(originalErr) ++ System.setErr(originalErr) + } + + void "getRequestLogMessage appends auditor when logAuditor is enabled and the lookup returns a value"() { given: -- def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> true - config.getProperty('grails.exceptionresolver.params.exclude', List, _) >> [null, 'password', 'token'] - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false -- config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false -- config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false -- config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false -- def grailsApp = Mock(GrailsApplication) -- grailsApp.getConfig() >> config -- def resolver = new GrailsExceptionResolver() -- resolver.grailsApplication = grailsApp - resolver.auditorAwareLookup = new AuditorAwareLookup(null) { - @Override - Optional<?> getCurrentAuditor() { Optional.of('alice') } - } - def request = new MockHttpServletRequest('GET', '/test') - def request = new MockHttpServletRequest('POST', '/login') - request.addParameter('Password', 'secret') - request.addParameter('apiToken', 'visible') - request.addParameter('TOKEN', 'abc123') - request.addParameter('username', 'sherlock') ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ resolver.auditorAwareLookup = new AuditorAwareLookup(null) { ++ ++ @Override ++ Optional<?> getCurrentAuditor() { Optional.of('alice') } ++ } ++ def request = new MockHttpServletRequest('GET', '/test') when: -- def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') then: - msg.contains('(user: alice)') - msg.contains('Password: ***') - msg.contains('TOKEN: ***') - msg.contains('username: sherlock') - msg.contains('apiToken: visible') - !msg.contains('Password: secret') - !msg.contains('TOKEN: abc123') ++ msg.contains('(user: alice)') } + void "getRequestLogMessage omits auditor when logAuditor is disabled"() { + given: - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() - resolver.grailsApplication = grailsApp - resolver.auditorAwareLookup = new AuditorAwareLookup(null) { - @Override - Optional<?> getCurrentAuditor() { Optional.of('alice') } - } - def request = new MockHttpServletRequest('GET', '/test') ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ resolver.auditorAwareLookup = new AuditorAwareLookup(null) { ++ ++ @Override ++ Optional<?> getCurrentAuditor() { Optional.of('alice') } ++ } ++ def request = new MockHttpServletRequest('GET', '/test') + + when: - def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') + + then: - !msg.contains('(user:') ++ !msg.contains('(user:') + } + + void "getRequestLogMessage omits auditor when logAuditor is enabled but auditor is absent"() { + given: - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() - resolver.grailsApplication = grailsApp - resolver.auditorAwareLookup = new AuditorAwareLookup(null) { - @Override - Optional<?> getCurrentAuditor() { Optional.empty() } - } - def request = new MockHttpServletRequest('GET', '/test') ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ resolver.auditorAwareLookup = new AuditorAwareLookup(null) { ++ ++ @Override ++ Optional<?> getCurrentAuditor() { Optional.empty() } ++ } ++ def request = new MockHttpServletRequest('GET', '/test') + + when: - def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') + + then: - !msg.contains('(user:') ++ !msg.contains('(user:') + } + + void "getRequestLogMessage appends remote address when logRemoteAddr is enabled"() { + given: - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() - resolver.grailsApplication = grailsApp - def request = new MockHttpServletRequest('GET', '/test') - request.remoteAddr = '198.51.100.42' ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ def request = new MockHttpServletRequest('GET', '/test') ++ request.remoteAddr = '198.51.100.42' + + when: - def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') + + then: - msg.contains('(ip: 198.51.100.42)') - !msg.contains('user:') ++ msg.contains('(ip: 198.51.100.42)') ++ !msg.contains('user:') + } + + void "getRequestLogMessage combines remote address and auditor into a single clause when both are enabled"() { + given: - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() - resolver.grailsApplication = grailsApp - resolver.auditorAwareLookup = new AuditorAwareLookup(null) { - @Override - Optional<?> getCurrentAuditor() { Optional.of(42L) } - } - def request = new MockHttpServletRequest('GET', '/test') - request.remoteAddr = '198.51.100.42' ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ resolver.auditorAwareLookup = new AuditorAwareLookup(null) { ++ ++ @Override ++ Optional<?> getCurrentAuditor() { Optional.of(42L) } ++ } ++ def request = new MockHttpServletRequest('GET', '/test') ++ request.remoteAddr = '198.51.100.42' + + when: - def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') + + then: - msg.contains('(ip: 198.51.100.42, user: 42)') ++ msg.contains('(ip: 198.51.100.42, user: 42)') + } + + void "subclasses can override resolveRemoteAddr to supply a custom IP extraction"() { + given: - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() { - @Override - protected String resolveRemoteAddr(HttpServletRequest req) { - req.getHeader('X-Forwarded-For') ?: req.remoteAddr - } ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() { ++ ++ @Override ++ protected String resolveRemoteAddr(HttpServletRequest req) { ++ req.getHeader('X-Forwarded-For') ?: req.remoteAddr + } - resolver.grailsApplication = grailsApp - def request = new MockHttpServletRequest('GET', '/test') - request.remoteAddr = '10.0.0.1' - request.addHeader('X-Forwarded-For', '203.0.113.7') ++ } ++ resolver.grailsApplication = grailsApp ++ def request = new MockHttpServletRequest('GET', '/test') ++ request.remoteAddr = '10.0.0.1' ++ request.addHeader('X-Forwarded-For', '203.0.113.7') + + when: - def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') + + then: - msg.contains('(ip: 203.0.113.7)') ++ msg.contains('(ip: 203.0.113.7)') + } + + void "AuditorAwareLookup returns empty when no application context is provided"() { + given: - def lookup = new AuditorAwareLookup(null) ++ def lookup = new AuditorAwareLookup(null) + + expect: - !lookup.getCurrentAuditor().isPresent() ++ !lookup.getCurrentAuditor().isPresent() + } + + void "logFullStackTraceIfEnabled emits the unfiltered trace when opt-in is enabled, and filterStackTrace then removes internal frames so the resolver log only sees the filtered trace"() { + given: "Captured System.err" - def originalErr = System.err - def baos = new ByteArrayOutputStream() - System.setErr(new PrintStream(baos, true)) ++ def originalErr = System.err ++ def baos = new ByteArrayOutputStream() ++ System.setErr(new PrintStream(baos, true)) + + and: "A resolver whose config opts in to full stack trace logging" - def config = Mock(Config) - config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> true - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false - config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false - config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false - config.getProperty('grails.logging.stackTraceFiltererClass', Class, _) >> ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> true ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> false ++ config.getProperty('grails.logging.stackTraceFiltererClass', Class, _) >> + DefaultStackTraceFilterer - def grailsApp = Mock(GrailsApplication) - grailsApp.getConfig() >> config - def resolver = new GrailsExceptionResolver() - resolver.grailsApplication = grailsApp ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp + + and: "An exception with a mix of internal (filterable) and application frames" - def exception = new RuntimeException('boom') - exception.stackTrace = [ ++ def exception = new RuntimeException('boom') ++ exception.stackTrace = [ + new StackTraceElement('java.lang.reflect.Method', 'invoke', 'Method.java', 580), + new StackTraceElement('com.example.MyController', 'show', 'MyController.groovy', 10), - ] as StackTraceElement[] - def request = new MockHttpServletRequest('GET', '/test') ++ ] as StackTraceElement[] ++ def request = new MockHttpServletRequest('GET', '/test') + + when: "The real resolveException ordering runs: log full trace, filter, then log with request context" - resolver.logFullStackTraceIfEnabled(exception) - resolver.filterStackTrace(exception) - resolver.logStackTrace(exception, request) ++ resolver.logFullStackTraceIfEnabled(exception) ++ resolver.filterStackTrace(exception) ++ resolver.logStackTrace(exception, request) + + then: "Both loggers emit" - System.err.flush() - def captured = baos.toString() - captured.contains('ERROR StackTrace ') - captured.contains('Full Stack Trace:') - captured.contains('o.g.web.errors.GrailsExceptionResolver') || ++ System.err.flush() ++ def captured = baos.toString() ++ captured.contains('ERROR StackTrace ') ++ captured.contains('Full Stack Trace:') ++ captured.contains('o.g.web.errors.GrailsExceptionResolver') || + captured.contains('org.grails.web.errors.GrailsExceptionResolver') + + and: "The application frame appears in both log entries" - captured.count('com.example.MyController.show(MyController.groovy:10)') == 2 ++ captured.count('com.example.MyController.show(MyController.groovy:10)') == 2 + + and: "The internal frame appears only once — in the unfiltered StackTrace entry, not in the filtered resolver entry" - captured.count('java.lang.reflect.Method.invoke(Method.java:580)') == 1 ++ captured.count('java.lang.reflect.Method.invoke(Method.java:580)') == 1 + + cleanup: - System.setErr(originalErr) ++ System.setErr(originalErr) ++ } ++ ++ void "getRequestLogMessage masks excluded request parameters case-insensitively"() { ++ given: ++ def config = Mock(Config) ++ config.getProperty('grails.exceptionresolver.logRequestParameters', Boolean, _) >> true ++ config.getProperty('grails.exceptionresolver.params.exclude', List, _) >> [null, 'password', 'token'] ++ config.getProperty('grails.exceptionresolver.logAuditor', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logRemoteAddr', Boolean, false) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTraceOnFilter', Boolean, true) >> false ++ config.getProperty('grails.exceptionresolver.logFullStackTrace', Boolean, false) >> false ++ def grailsApp = Mock(GrailsApplication) ++ grailsApp.getConfig() >> config ++ def resolver = new GrailsExceptionResolver() ++ resolver.grailsApplication = grailsApp ++ def request = new MockHttpServletRequest('POST', '/login') ++ request.addParameter('Password', 'secret') ++ request.addParameter('apiToken', 'visible') ++ request.addParameter('TOKEN', 'abc123') ++ request.addParameter('username', 'sherlock') ++ ++ when: ++ def msg = resolver.getRequestLogMessage('RuntimeException', request, 'boom') ++ ++ then: ++ msg.contains('Password: ***') ++ msg.contains('TOKEN: ***') ++ msg.contains('username: sherlock') ++ msg.contains('apiToken: visible') ++ !msg.contains('Password: secret') ++ !msg.contains('TOKEN: abc123') + } }
