Skip to content

Fix SAX parser feature URIs and reject DOCTYPE declarations - #16310

Closed
jamesfredley wants to merge 3 commits into
apache:8.0.xfrom
jamesfredley:fix/xml-parser-hardening
Closed

Fix SAX parser feature URIs and reject DOCTYPE declarations#16310
jamesfredley wants to merge 3 commits into
apache:8.0.xfrom
jamesfredley:fix/xml-parser-hardening

Conversation

@jamesfredley

Copy link
Copy Markdown
Contributor

Summary

ASF security review finding f002: SAX XXE hardening in SpringIOUtils.createParserFactory set parser features with https://xml.org/... and https://apache.org/.... The registered JAXP / Xerces identifiers are the http:// forms, so the factory rejected the features and the empty catch hid the failure. XmlDataBindingSourceCreator still uses SpringIOUtils.createXmlSlurper() for application/xml request bodies. The HTTP test-client XmlUtils had the same identifiers and left disallow-doctype-decl false.

This is hardening, not a HIGH CVE against the current threat model (THREAT_MODEL.md §9 disclaims parser configuration / XXE). No threat-model change in this PR.

Changes

  • Use the registered http://xml.org/... and http://apache.org/... SAX feature URIs.
  • Set disallow-doctype-decl to true and disable XInclude.
  • Apply the same defaults in the HTTP test-client XML slurper.
  • Cover DOCTYPE rejection in SpringIOUtilsSpec, XmlUtilsSpec, and TestHttpResponseSpec.

Testing

  • :grails-gradle:grails-gradle-model:test
  • :grails-testing-support-http-client:test
  • :grails-gradle:grails-gradle-model:codeStyle
  • :grails-testing-support-http-client:codeStyle

The SAX XXE hardening used https://xml.org and https://apache.org feature
identifiers. Xerces only recognizes the http:// forms, so the empty catch
swallowed the misconfiguration. XmlDataBindingSourceCreator and the HTTP
test client both now set the registered http:// identifiers, disable
XInclude, and reject DOCTYPE declarations.
Copilot AI lite review requested due to automatic review settings September 3, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

New setXIncludeAware(false) calls should be guarded for parser compatibility, and the updated DOCTYPE tests should more directly validate “reject all DOCTYPE” behavior (including internal subsets) to prevent regressions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR hardens XML parsing defaults across SpringIOUtils and the HTTP test-client XmlUtils by fixing SAX feature URIs (using the registered http:// identifiers) and enforcing DOCTYPE rejection to reduce XXE/DTD-related attack surface.

Changes:

  • Fix SAX feature URI schemes (https://http://) so secure parser features are actually applied.
  • Change defaults to reject DOCTYPE declarations and disable XInclude.
  • Update HTTP test-client behavior and docs, and adjust/add tests for DOCTYPE rejection.
File summaries
File Description
grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/utils/XmlUtilsSpec.groovy Updates XML slurper security tests for DOCTYPE rejection.
grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/TestHttpResponseSpec.groovy Updates response XML parsing tests to reflect hardened defaults.
grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/utils/XmlUtils.groovy Fixes feature URIs, rejects DOCTYPE, and disables XInclude in the default SAX factory.
grails-testing-support-http-client/README.md Updates documentation to reflect DOCTYPE rejection behavior.
grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy Adds coverage for hardened SpringIOUtils.createXmlSlurper() behavior.
grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java Fixes feature URIs, enforces DOCTYPE rejection, and disables XInclude in SAX factory creation.
grails-doc/src/en/guide/testing/integrationTesting.adoc Updates user docs for secure default XML parsing behavior in HTTP client testing.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java Outdated
@jamesfredley

Copy link
Copy Markdown
Contributor Author

@copilot re-review please. Follow-up 6fdc508 guards XInclude setXIncludeAware and adds internal-subset DOCTYPE rejection tests.

The SAX feature identifiers were declared twice, once in SpringIOUtils and
once in the HTTP test client, as bare string literals. That duplication is
how the https:// spelling was introduced and went unnoticed: setFeature
answers an unrecognised name with SAXNotRecognizedException, and both call
sites swallow it, so the hardening silently switched off.

Collect the five identifiers in XmlParserFeature in grails-gradle-common,
which sits in the grails-gradle build alongside SpringIOUtils and is already
exposed to the root build by grails-common. The enum documents that the
values are registered identifiers rather than addresses, and carries the
reason the http scheme cannot be rewritten.

Keep rejecting DOCTYPE declarations by default, and add
grails.xml.allowDocTypeDeclaration for applications that must accept them.
SpringIOUtils reads it through Metadata, so it is set in application.yml or
as a system property. Opting in relaxes only whether a declaration is
permitted; external general entities, external parameter entities and
external DTDs stay refused either way, so it does not reopen the XXE vector.

The setting is needed because this parser factory is shared with readers of
trusted classpath descriptors -- TldReader, WebXmlTagLibraryReader and
PluginUtils -- and TLDs routinely carry a DOCTYPE.
jakarta.servlet.jsp.jstl ships eight, including c-1_0-rt.tld, which the
default grails.gsp.tldScanPattern scans, so an application resolving JSP tag
libraries from a GSP needs it enabled. Document that on the JSP tag library
page and in the upgrade notes.

Cover both modules with tests that assert observable behaviour rather than
reading feature flags back, so no test holds a second copy of the
identifiers that a rewrite could update in step with the production code.
XmlParserFeatureSpec additionally asserts every identifier is one a parser
actually registers, turning an unrecognised name into a named failure
instead of a silent no-op.

@jdaugherty jdaugherty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed the premise before anything else: against the JDK parser all six http:// identifiers are accepted and every https:// form is answered with SAXNotRecognizedException, so the previous values were setting nothing at all. The correction is real, and the strict DOCTYPE default you picked is the right one to ship. Everything below is about keeping it that way.

I've opened jamesfredley#4 against this branch with a suggested implementation of the three points below, so they're easy to take or drop.

Where the https spelling came from. eed8df3594 ("chore: update url to HTTPS", #13478) rewrote them across 79 files in April 2024. That sweep was preparatory work toward adopting io.spring.nohttp — the tool itself has never run over these modules and would not have flagged these: its default allowlist carries ^http://xml\.org/.* and ^http://apache\.org/xml/features/.*, so adopting it needs no exception for them. The hazard was the manual preparation, not the tool. Worth calling out because if we make another run at adopting nohttp the same well-meant rewrite can land again, and the empty catches around setFeature will hide it again exactly as they did for two years. For this reason, I'm suggesting we centralize the config into an enum constant that we document this risk explicitly.

DOCTYPE rejection stops JSP tag library resolution, with nothing configurable to get past it. createParserFactory() returns one cached factory shared with TldReader and WebXmlTagLibraryReader, which read classpath descriptors that legitimately carry a DOCTYPE. GspAutoConfiguration scans classpath*:/META-INF/c-1_0-rt.tld by default, and that file in org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1 opens with a JSP 1.2 DOCTYPE — eight of the twenty TLDs in that jar have one. TagLibraryResolverImpl.initialize() doesn't catch, so it surfaces from the first resolveTagLibrary(uri) call. No existing test covers it (the GSP tests use a fixture TLD without a DOCTYPE), so CI stays green.

Rather than weaken the default, #4 adds grails.xml.allowDocTypeDeclaration, read through Metadata so it works from application.yml or as a system property, defaulting to off. Applications opt in knowing what they're accepting, and it relaxes only whether a declaration is permitted — external entities and external DTDs stay refused either way. I'm really not sure this is a "true" vulnerability since we use this utility to parse developer code, but the config seems like a sensible default if we're going to continue to support JSP.

The external-entity features end up untested. The other four identifiers can't be reached by any test once DOCTYPE is rejected, and the deleted tests were their only coverage. Given this PR exists because a wrong identifier went unnoticed, worth not leaving those four in the same position.

The identifiers are declared twice, here and in the HTTP test client, as bare literals in both — which is how one rewrite became two. #4 collects them in an enum in grails-gradle-common (same build as SpringIOUtils; grails-common already re-exposes it via api 'org.apache.grails.gradle:grails-gradle-common', so the test client sees it too), with the reason the scheme is fixed written next to the values.


try {
saxParserFactory.setFeature("https://apache.org/xml/features/disallow-doctype-decl", false);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This default is right, but nothing lets an application past it, and one common setup needs to get past it.

This factory is shared with TldReader and WebXmlTagLibraryReader, which read TLDs and web.xml from the classpath. GspAutoConfiguration sets the default grails.gsp.tldScanPattern to include classpath*:/META-INF/c-1_0-rt.tld, and that file in jakarta.servlet.jsp.jstl:3.0.1 begins

<!DOCTYPE taglib
  PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
  "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">

Parsing it with this feature set gives SAXParseException: DOCTYPE is disallowed when the feature "http://apache.org/xml/features/disallow-doctype-decl" set to true. TagLibraryResolverImpl.initialize() doesn't catch, so it surfaces from the first resolveTagLibrary(uri) call.

A hint from our own code: TldReader falls back to tag.'tagclass'.text(), the JSP 1.1 element name, which only appears in DTD-era TLDs — the reader is written to handle exactly the documents this now rejects.

jamesfredley#4 keeps this true and adds grails.xml.allowDocTypeDeclaration, read via Metadata so it comes from application.yml or a system property, off by default. Opting in doesn't reopen XXE: with declarations permitted, a file:// external entity still resolves to empty rather than the file contents, which the tests there assert.

saxParserFactory = FactorySupport.createSaxParserFactory();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setValidating(false);
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These five are also declared in XmlUtils in the HTTP test client, as bare literals in both places. That duplication is how a single sweep became two broken call sites.

eed8df3594 (#13478) is where the https spelling came from — a 79-file pass done as preparation for adopting io.spring.nohttp. Since we still intend to adopt it, this can happen again, which is the argument for putting the values somewhere with the explanation attached rather than leaving them as bare literals in two files.

For what it's worth, nohttp itself would not have flagged these: its default allowlist already carries ^http://xml\.org/.* and ^http://apache\.org/xml/features/.*. Adopting the tool needs no exception for them, and its allowlist is a decent reference for which http:// strings here are identifiers rather than links.

jamesfredley#4 collects them in an enum in grails-gradle-common — same build as this module, and already visible to the root build through grails-common.

then:
thrown(SAXParseException)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assert DOCTYPE rejection, which leaves external-general-entities, external-parameter-entities, load-dtd-grammar and load-external-dtd with no coverage at all — once DOCTYPE is refused, no document can reach them. The tests removed from XmlUtilsSpec and TestHttpResponseSpec were the only thing pinning them, so the same wrong-identifier mistake in those four lines would still pass CI.

One thing worth knowing when adding coverage: a test that reads the flags back with getFeature(...) has to restate the identifiers, so a sweep rewrites the test and the production code together. It does fail, but at the test, with a name-lookup error whose tempting fix is to make the test tolerate it. Driving a document with an external entity through the parser and asserting the file contents don't appear keeps the assertion free of any identifier.

jamesfredley#4 does that, and adds a spec deriving the list from values() that asserts each identifier is one a parser actually registers — so an unrecognised name fails by name instead of silently. I checked both failure modes against it: rewriting the enum to https fails 4 of 7 behavioural cases plus all 5 of those.

}

void 'newXmlSlurper allows inline doctype declarations with internal entities'() {
void 'newXmlSlurper rejects doctype declarations with external entities'() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small one: parsed is assigned but the block only asserts thrown(SAXParseException), so the variable is dead. Same in the internal-entity test below.

}

void 'xml uses a secure default slurper that does not resolve external entities'() {
void 'xml rejects doctype declarations with external entities'() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping e.message.contains('External Entity') leaves both tests asserting a bare SAXParseException, which a merely malformed document would also satisfy. Asserting on 'DOCTYPE is disallowed' would keep them pinned to the behaviour they're named for.

Formatting nit: the fixture here indents <!ENTITY and ]> by one space while the internal-entity fixture below doesn't.

* <p>
* The default parser is namespace aware, non-validating, permits inline DOCTYPE declarations,
* and disables external entity expansion plus external DTD loading.
* The default parser is namespace aware, non-validating, and rejects DOCTYPE declarations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This javadoc drops "disables external entity expansion plus external DTD loading", but those features are still being set. The README and integrationTesting.adoc both kept the sentence, so it's only the javadoc that loses it.

No change needed to the strict behaviour here — a test client refusing DOCTYPE is the right call, and #4 leaves it exactly as you have it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Mandatory DOCTYPE protections can still fail open when SAX providers reject the feature.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

grails-doc/src/en/guide/testing/integrationTesting.adoc:503

  • This documents the new test-response behavior, but the same SpringIOUtils change also makes Grails application/xml, text/xml, and HAL XML request binding reject DOCTYPEs. The REST binding documentation remains silent about that new input restriction; add it there (and a migration note if this targets an existing release) so application users are not surprised by rejected request bodies.
rejects `DOCTYPE` declarations, and disables external entity expansion plus external DTD loading.
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Balanced


try {
saxParserFactory.setFeature("https://apache.org/xml/features/disallow-doctype-decl", false);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

private static final Map<String, Boolean> SECURE_XML_SLURPER_FEATURES = [
(DISALLOW_DOCTYPE_DECL): false,
(DISALLOW_DOCTYPE_DECL): true,
@jdaugherty

Copy link
Copy Markdown
Contributor

All of my review feedback is implemented in jamesfredley#4 - I made this configurable given that it can break JSPs

@matrei

matrei commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

AI Review Findings

Head 6fdc50880a on 8.0.x base 55d5076aa5. I reproduced the premise independently against the JDK 21 parser (com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl): every http:// identifier is accepted and every https:// spelling is answered with SAXNotRecognizedException. The sweep that introduced the https forms is eed8df3594 (#13478, 79 files), so since April 2024 createParserFactory() has configured nothing beyond FEATURE_SECURE_PROCESSING, and the catch (Exception) around each call kept that invisible. The correction is real and the direction is right.

There is one blocking problem with the default this PR ships, and it is not covered by any test in the repository. The follow-up in jamesfredley#4 addresses it with a global opt-in; I would take that PR with one change in shape (see the review on jamesfredley#4).

[P1] Rejecting DOCTYPE in the shared factory breaks JSP tag library resolution for every application that uses JSTL

Files:

  • grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java:433
  • grails-gsp/grails-web-jsp/src/main/groovy/org/grails/gsp/jsp/TldReader.groovy:49
  • grails-gsp/grails-web-jsp/src/main/groovy/org/grails/gsp/jsp/TagLibraryResolverImpl.groovy:71-90
  • grails-gsp/spring-boot/src/main/java/grails/gsp/boot/GspAutoConfiguration.java:323

createParserFactory() returns one cached factory for every caller of createXmlSlurper() and newSAXParser(). Those callers fall into two trust levels:

Caller Input
XmlDataBindingSourceCreator, HalXmlDataBindingSourceCreator, grails.converters.XML.parse HTTP request bodies (untrusted)
TldReader, WebXmlTagLibraryReader, PluginUtils Descriptors on the application classpath (trusted)

The descriptors are where the strict default bites. GspAutoConfiguration defaults grails.gsp.tldScanPattern to a list that ends with classpath*:/META-INF/c-1_0-rt.tld. In org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.1 that file opens with a JSP 1.2 DOCTYPE, and so do 8 of the 22 descriptors in the jar (c-1_0*.tld, fmt-1_0*.tld, sql-1_0*.tld, x-1_0*.tld). TagLibraryResolverImpl.initialize() scans every pattern in one loop and catches nothing, so the first resolveTagLibrary(uri) call throws SAXParseException: DOCTYPE is disallowed ... and no JSP tag library resolves, including jakarta.tags.core from the DOCTYPE-free c.tld that was scanned earlier in the same loop.

I confirmed this with a spec in grails-gsp/plugin (JSTL is already on that module's test runtime classpath) that scans c-1_0-rt.tld plus c.tld and resolves jakarta.tags.core. On this head it fails with DOCTYPE is disallowed. The existing GSP tests stay green only because GroovyPageWithJSPTagsTests, AbstractGrailsTagTests and TagLibraryResolverTests scan c.tld, fmt.tld, core.tld and spring*.tld, none of which carries a DOCTYPE; TldReaderTests uses a fixture without one. The two test examples that put JSTL on the runtime classpath (gsp-layout, gsp-sitemesh3) contain no <%@ taglib %> directive, so the resolver is never initialised there either.

The documented custom pattern classpath*:/META-INF/*.tld in usingJSPTagLibraries.adoc hits all eight.

The user-visible symptom is the worst kind: a 500 on the first page that uses a JSP tag, with a parser error that names a feature URI nobody set in the application.

What I would change. The two trust levels want two parsers, and the readers of trusted descriptors know they are reading trusted descriptors. Keep the strict default for request bodies and let the descriptor readers ask for DOCTYPE tolerance explicitly:

// SpringIOUtils
public static XmlSlurper createXmlSlurper() throws ... {            // strict, for request bodies
    return createXmlSlurper(false);
}

public static XmlSlurper createXmlSlurper(boolean allowDocTypeDeclaration) throws ... {
    return new XmlSlurper(createParserFactory(allowDocTypeDeclaration).newSAXParser());
}

with TldReader, WebXmlTagLibraryReader and PluginUtils passing true. External general and parameter entities, DTD grammar loading and external DTD retrieval stay off on both factories, so the tolerant parser still resolves a file:// entity to nothing and skips the web-jsptaglibrary_1_2.dtd reference instead of fetching it. That needs no configuration key, no upgrade note for JSP users, and does not relax request-body parsing as a side effect of using JSTL.

jamesfredley#4 instead adds grails.xml.allowDocTypeDeclaration, read through Metadata, that switches the single shared factory. It works, and I ran its tests, but it makes "I use JSTL" and "accept DOCTYPE in request bodies" the same switch. Details in that review.

[P2] A rejected hardening feature is still swallowed silently

Files:

  • grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java:431-461
  • grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/utils/XmlUtils.groovy:235-243

The PR exists because catch (Exception) { /* ignore */ } hid a SAXNotRecognizedException for two years, and the catch blocks are unchanged. The tolerance for parsers lacking a feature is reasonable, but it should leave a trace. grails-gradle-model already has slf4j-api as an api dependency; a LOG.warn("Parser {} does not support feature {}", factory.getClass(), name) inside each catch costs nothing and would have surfaced this in every application log since 2024. Copilot's suggestion to make disallow-doctype-decl mandatory and propagate is too strong for a shared factory, but a warning is not.

jamesfredley#4 adds XmlParserFeatureSpec, which asserts each identifier is recognised by the JDK parser. That pins the identifiers at test time; the warning covers the runtime case where a different SAX provider is first on the classpath.

[P2] The four external-entity features lose their only behavioural coverage

Files:

  • grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy
  • grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/utils/XmlUtilsSpec.groovy:279-300
  • grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/TestHttpResponseSpec.groovy:201-227

Every new test asserts DOCTYPE rejection, and once a DOCTYPE is refused no document reaches external-general-entities, external-parameter-entities, load-dtd-grammar or load-external-dtd. The deleted newXmlSlurper blocks external entities and xml uses a secure default slurper that does not resolve external entities were the only tests that drove an entity through the parser and asserted the file contents did not appear. After this PR the same wrong identifier in any of those four lines passes CI again. jamesfredley#4 restores this coverage for SpringIOUtils by testing with DOCTYPE permitted; the split-factory shape above makes that natural, because the tolerant factory is the one that can be tested for entity blocking.

[P2] THREAT_MODEL.md §9 now states something the code no longer does

Files:

  • THREAT_MODEL.md:346
  • THREAT_MODEL.md:470

The description says "No threat-model change in this PR", but §9 currently reads "XXE in XML data binding. XML parsing is delegated to the underlying parser; the framework does not impose a parser configuration. (inferred)", and §14 question 13 proposes confirming exactly that. After this PR the framework does impose a configuration on every XML request body it binds, and the strict DOCTYPE default is a behaviour change users will hit. The sentence in §9 should move to §8 or be rewritten to describe what is now guaranteed (external entities and external DTDs refused, DOCTYPE refused unless opted in), and Q13 should be answered rather than left open. Leaving the disclaimer in place means the next security review will re-report f002 against a document that says the fix does not exist.

Related: the upgrade notes need to say that application/xml, text/xml and HAL XML request bodies carrying a DOCTYPE are now refused. integrationTesting.adoc only covers the test client. jamesfredley#4 adds an "XML Parsing Defaults" section to upgrading.adoc that does this.

[P3] Small things in the test client, still present on jamesfredley#4's head

  • XmlUtils.groovy:116 javadoc drops "disables external entity expansion plus external DTD loading" although those features are still set; the README and integrationTesting.adoc kept the sentence.
  • XmlUtilsSpec.groovy:281,292: def parsed = is assigned and never read.
  • TestHttpResponseSpec.groovy:201-227 and XmlUtilsSpec.groovy:279-300: both tests now assert a bare thrown(SAXParseException), which a malformed document also satisfies. e.message.contains('DOCTYPE is disallowed') pins them to the behaviour their names claim.
  • The external-entity fixtures indent <!ENTITY and ]> by one space; the internal-entity fixtures next to them do not.

Verified

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Review follow-up that was mistakenly opened on the archive fork is now #16328

@jamesfredley
jamesfredley force-pushed the fix/xml-parser-hardening branch from d4c5ac0 to 6fdc508 Compare September 9, 2026 13:51
@jamesfredley

Copy link
Copy Markdown
Contributor Author

This PR now includes jdaugherty's review follow-up (previously jamesfredley#4 / #16328).

@jamesfredley

Copy link
Copy Markdown
Contributor Author

Superseded by #16331 so the head branch is on apache/grails-core (not the archive fork).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants