fix: reject unsupported JSP/Groovy declaration blocks in GSP parser#15398
fix: reject unsupported JSP/Groovy declaration blocks in GSP parser#15398jamesfredley wants to merge 1 commit intoapache:7.0.xfrom
Conversation
GSP declaration blocks (<%! ... %> and !{ ... }!) have never worked
correctly — the declare() method wrote content before the class
definition during pass 1, creating a broken script+class hybrid that
fails at runtime with a 500 error and no useful diagnostic.
Replace the broken declare() implementation with a clear
GrailsTagException that tells the developer exactly what syntax is
unsupported and suggests alternatives (<% %>, controller, or service).
Fixes: <%! int x = 0; %> now fails at parse time with a descriptive
error instead of silently generating broken bytecode.
- Change declare() in GroovyPageParser to throw GrailsTagException
- Add 3 Spock tests covering JSP (<%! %>), JSP with method, and
Groovy (!{ }!) declaration block variants
- All 13 ParseSpec tests pass (10 existing + 3 new)
| } | ||
|
|
||
| private void declare(boolean gsp) { | ||
| if (finalPass) { |
There was a problem hiding this comment.
Why did you remove the final pass check?
There was a problem hiding this comment.
The finalPass guard is irrelevant because the new code unconditionally throws a GrailsTagException. Since declare() is called during pass 1 (when finalPass=false), the exception fires immediately on the first encounter. There's no reason to let pass 1 succeed and then throw on pass 2 - the guard would be dead code after the throw.
|
The final pass check was removed to change the behavior of the declare method. Previously, it would skip processing during the final pass, but now it unconditionally throws an exception to disable support for JSP-style declaration blocks in Groovy Server Pages. |
| } | ||
|
|
||
| out.println(); | ||
| write(scan.getToken().trim(), gsp); |
There was a problem hiding this comment.
This doesn't look like the right change to me. This was throwing an error before - are you saying this declare worked in previous versions? @davydotcom did rework some of this in 7.x, is this really just a bug introduced in 7.x?
There was a problem hiding this comment.
The old declare() was not throwing an error. It was calling write(scan.getToken().trim(), gsp) during pass 1, which silently emits content to the output stream. Because pass 2 (where the class body is generated) hasn't run yet, this content ends up placed between the package statement and the generated class declaration:
// Generated code (broken):
package com.example.demo
int counter = 0; // written by declare() in pass 1
class DemoPage extends GroovyPage {
void run() { ... } // written in pass 2
}Groovy compiles this without error (it's a valid script+class hybrid), but at runtime the class body can't see the script-level variable, causing a MissingPropertyException (500 error with no diagnostic pointing to the declaration block).
This is not a 7.x regression
The declare() method body is identical to the original 2009 commit (3e17890a22). David Estes's 7.x optimization commit (85148d4612, Oct 2025) only touched the class javadoc and renamed printHtmlPart( to h( in htmlPartPrintlnRaw() - he did not touch declare() or the two-pass logic at all.
The core issue is that declare() has inverted finalPass logic compared to every other code-generating method in the parser:
| Method | Guard | Runs in | Purpose |
|---|---|---|---|
script() |
if (!finalPass) return |
Pass 2 | Code generation |
expr() |
if (!finalPass) return |
Pass 2 | Code generation |
html() |
if (!finalPass) return |
Pass 2 | Code generation |
scriptletExpr() |
if (!finalPass) return |
Pass 2 | Code generation |
startTag() |
if (!finalPass) return |
Pass 2 | Code generation |
endTag() |
if (!finalPass) return |
Pass 2 | Code generation |
direct() |
if (finalPass) return |
Pass 1 | Directive processing (correct - configures parser state) |
declare() |
if (finalPass) return |
Pass 1 | Writes code output (broken - ends up before class definition) |
declare() writes code like script() does, but runs in pass 1 like direct() does. That mismatch is the bug, and it has existed since Grails 1.1 (March 2009).
There was a problem hiding this comment.
have you actually tested that this is an issue in Grails 6 though? This looks like an AI response and I want to understand that we actually created an app to ensure this isn't really a borken feature.
There was a problem hiding this comment.
With https://github.com/jamesfredley/grails-gsp-declaration-block-bug it can be reproduced. The reason this was not brought up for fixed before is likely that <%! int counter = 0; %> has been a known anti-pattern for a long time in JSP and most user didn't try it in GSP because it is not a good idea. This is a super edge case, but does produce a runtime exception if you do it.
There was a problem hiding this comment.
The app you linked is a Grails 7 app though. Is there a branch where you tested this with Grails 6?
There was a problem hiding this comment.
Corrected my comment, I originally said Grails 6 instead of Grails 7.
There was a problem hiding this comment.
I did not, but that code has been present since Grails 1.1, so the outcome should be the same. Users in general know <%! int counter = 0; %> is a bad idea, but if they try it, it compiles but then errors during runtime. This PR is just trying to alert them during the build instead of at runtime. The outcome is the same, an exception.
There was a problem hiding this comment.
My fear in this area is the GSP dependency structure was significantly changed in Grails 7 and the area needs a lot of work. We should first confirm this isn't a regression before removing it.
There was a problem hiding this comment.
Pull request overview
This PR updates the GSP compiler to explicitly reject JSP/Groovy declaration block syntaxes that historically generated invalid Groovy output and caused confusing runtime failures, replacing them with a clear compile-time GrailsTagException.
Changes:
- Replace
GroovyPageParser.declare()codegen with an explicitGrailsTagExceptionexplaining the unsupported syntax and suggested alternatives. - Add Spock coverage ensuring both
<%! ... %>and!{ ... }!declaration blocks are rejected during parsing.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java | Throws a compile-time GrailsTagException when declaration blocks are encountered instead of generating broken Groovy output. |
| grails-gsp/plugin/src/test/groovy/org/grails/web/pages/ParseSpec.groovy | Adds tests asserting parser rejection and message content for both declaration block syntaxes. |
Comments suppressed due to low confidence (1)
grails-gsp/core/src/main/groovy/org/grails/gsp/compiler/GroovyPageParser.java:470
- The error message always says "JSP-style declaration blocks" even when
gspis true (syntax!{ ... }!). That wording is contradictory/misleading for the Groovy/GSP form. Consider making the wording conditional (e.g., "GSP-style" for!{...}!) or using a neutral phrase like "Declaration blocks" for both forms.
String syntax = gsp ? "!{ ... }!" : "<%! ... %>";
throw new GrailsTagException(
"JSP-style declaration blocks (" + syntax + ") are not supported in Groovy Server Pages. " +
"Use <% ... %> for scriptlet code or move logic to a controller or service.",
pageName, getCurrentOutputLineNumber());
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
GSP declaration blocks (
<%! ... %>and!{ ... }!) have never worked correctly in any Grails version. Thedeclare()method inGroovyPageParser.javagenerates broken Groovy code that causes a 500 Internal Server Error at runtime with no useful diagnostic. This fix replaces the broken implementation with a clear compile-timeGrailsTagExceptionthat tells the developer exactly what syntax is unsupported and suggests alternatives.Bug Description
When a GSP contains a JSP-style declaration block:
The application compiles and starts normally, but rendering the page at runtime produces a 500 Internal Server Error. The error message gives no indication that the declaration block syntax is the cause.
Before (broken — 500 error, no diagnostic):
After (clear compile-time error):
Root Cause
The GSP parser (
GroovyPageParser.java) uses a two-pass system:finalPass=false): Pre-scan. Most handlers skip viaif (!finalPass) return;finalPass=true): Code generation. This is where the class body is writtenThe
declare()method uniquely has inverted logic —if (finalPass) return;— so it runs during Pass 1 only. This causes declaration content to be written to the output stream before the class definition, between thepackagestatement andclassdeclaration:This creates an invalid Groovy script+class hybrid. Groovy compiles it without error, but at runtime the class body cannot see the script-level variable, causing
MissingPropertyException.This has been broken since the very first commit in Grails 1.1 (March 2009). The
declare()method has never generated correct code. Zero real-world usage exists — searching all public GitHub repositories for<%!in.gspfiles returns exactly 1 result (a Sublime Text syntax highlighting test file). The Groovy variant!{ ... }!has 0 results.Fix
Replaced the broken
declare()implementation with aGrailsTagExceptionthrow, following the same error pattern used throughoutGroovyPageParser.java:The error is thrown during parsing (before any code generation), so developers get immediate feedback with the exact file name, line number, and a suggestion for the correct alternative syntax.
Files Changed
grails-gsp/core/.../GroovyPageParser.javadeclare()to throwGrailsTagExceptioninstead of writing broken codegrails-gsp/plugin/.../ParseSpec.groovyTest Coverage
3 new Spock tests added to
ParseSpec.groovy(all 13 tests pass — 10 existing + 3 new):parse with JSP declaration block throws error— verifies<%! int counter = 0; %>throwsGrailsTagExceptionwith message containing "declaration blocks", "<%! ... %>", and "not supported"parse with JSP declaration block containing method throws error— verifies<%! String hello() { return "hi"; } %>embedded in HTML throwsGrailsTagExceptionparse with Groovy declaration block throws error— verifies!{ int counter = 0; }!(Groovy variant) throwsGrailsTagExceptionwith message containing "!{ ... }!"Example Application
Repository: https://github.com/jamesfredley/grails-gsp-declaration-block-bug
Grails 7.0.7 app with two pages:
http://localhost:8080/bugDemo/safe— uses<% int counter = 0; %>(scriptlet) — renders correctlyhttp://localhost:8080/bugDemo/index— uses<%! int counter = 0; %>(declaration block) — 500 error (demonstrates the bug)Environment Information
Version
7.0.7