This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 4027a4e0cb16e313a9acfe37baa56e6a2791a5c0 Author: opencode <[email protected]> AuthorDate: Thu Oct 8 15:04:00 2026 +0200 Report the configured error message when a conditional SSI directive fails The single catch site for SSIStopProcessingException in SSIProcessor.process() assumes the command that threw has already logged the failure and written the configured error message to the response. That assumption did not hold for SSIConditional, which threw bare when the expr attribute was missing or renamed and when the expression could not be parsed, and for the unknown conditional command branch. A malformed <!--#if--> therefore truncated the rest of the document from the response with no error message and no log entry, making the failure invisible and hard to diagnose. SSIConditional now logs the failure and writes the configured error message before throwing, mirroring the established error path of the other commands such as SSISet, using the new ssiConditional.* messages. In addition, SSIProcessor.process() now logs the exception whenever it carries a wrapped cause, which previously was discarded at the catch site by ExpressionParseTree.evaluateTree() wrapping arbitrary Throwables, and whenever debugging is enabled, so no failure path can remain silent. Document truncation at a failing directive is unchanged; it mimics Apache behaviour. Add TestSsiServlet.testBadConditionalExpressionReportsError to cover the end-to-end behaviour. --- .../apache/catalina/ssi/LocalStrings.properties | 4 +++ java/org/apache/catalina/ssi/SSIConditional.java | 16 ++++++++-- java/org/apache/catalina/ssi/SSIProcessor.java | 7 +++- test/org/apache/catalina/ssi/TestSsiServlet.java | 37 ++++++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/java/org/apache/catalina/ssi/LocalStrings.properties b/java/org/apache/catalina/ssi/LocalStrings.properties index 5cb718e6bd..5969272c58 100644 --- a/java/org/apache/catalina/ssi/LocalStrings.properties +++ b/java/org/apache/catalina/ssi/LocalStrings.properties @@ -25,6 +25,10 @@ expressionParseTree.unusedOpCodes=Unused nodes exist ssiCommand.invalidAttribute=Invalid attribute [{0}] +ssiConditional.errorEvaluatingExpression=Error evaluating expression [{0}] +ssiConditional.noExpression=No expression specified +ssiConditional.unknownCommand=Unknown conditional command [{0}] + ssiEcho.invalidEncoding=Invalid encoding [{0}] ssiExec.executeFailed=Cannot execute file [{0}] diff --git a/java/org/apache/catalina/ssi/SSIConditional.java b/java/org/apache/catalina/ssi/SSIConditional.java index d122298265..014f196eaf 100644 --- a/java/org/apache/catalina/ssi/SSIConditional.java +++ b/java/org/apache/catalina/ssi/SSIConditional.java @@ -20,11 +20,15 @@ package org.apache.catalina.ssi; import java.io.PrintWriter; import java.text.ParseException; +import org.apache.tomcat.util.res.StringManager; + /** * SSI command that handles all conditional directives. */ public class SSIConditional implements SSICommand { + private static final StringManager sm = StringManager.getManager(SSIConditional.class); + /** * Default constructor. @@ -50,7 +54,7 @@ public class SSIConditional implements SSICommand { } state.nestingCount = 0; // Evaluate the expression - if (evaluateArguments(paramNames, paramValues, ssiMediator)) { + if (evaluateArguments(paramNames, paramValues, ssiMediator, writer)) { // No more branches can be taken for this if block state.branchTaken = true; } else { @@ -71,7 +75,7 @@ public class SSIConditional implements SSICommand { return lastModified; } // Evaluate the expression - if (evaluateArguments(paramNames, paramValues, ssiMediator)) { + if (evaluateArguments(paramNames, paramValues, ssiMediator, writer)) { // Turn back on output and mark the branch state.processConditionalCommandsOnly = false; state.branchTaken = true; @@ -106,6 +110,8 @@ public class SSIConditional implements SSICommand { // in the first place. state.branchTaken = true; } else { + ssiMediator.log(sm.getString("ssiConditional.unknownCommand", commandName)); + writer.write(ssiMediator.getConfigErrMsg()); throw new SSIStopProcessingException(); } return lastModified; @@ -115,16 +121,20 @@ public class SSIConditional implements SSICommand { /** * Retrieves the expression from the specified arguments and performs the necessary evaluation steps. */ - private boolean evaluateArguments(String[] names, String[] values, SSIMediator ssiMediator) + private boolean evaluateArguments(String[] names, String[] values, SSIMediator ssiMediator, PrintWriter writer) throws SSIStopProcessingException { String expr = getExpression(names, values); if (expr == null) { + ssiMediator.log(sm.getString("ssiConditional.noExpression")); + writer.write(ssiMediator.getConfigErrMsg()); throw new SSIStopProcessingException(); } try { ExpressionParseTree tree = new ExpressionParseTree(expr, ssiMediator); return tree.evaluateTree(); } catch (ParseException e) { + ssiMediator.log(sm.getString("ssiConditional.errorEvaluatingExpression", expr), e); + writer.write(ssiMediator.getConfigErrMsg()); throw new SSIStopProcessingException(); } } diff --git a/java/org/apache/catalina/ssi/SSIProcessor.java b/java/org/apache/catalina/ssi/SSIProcessor.java index b489bfd691..2c4d04e039 100644 --- a/java/org/apache/catalina/ssi/SSIProcessor.java +++ b/java/org/apache/catalina/ssi/SSIProcessor.java @@ -181,7 +181,12 @@ public class SSIProcessor { } } } catch (SSIStopProcessingException e) { - // If we are here, then we have already stopped processing, so all is good + // If we are here, then processing has already been stopped by a command that + // reported its own error. Log any wrapped cause so nothing can fail silently, + // and log everything when debugging is enabled. + if (debug > 0 || e.getCause() != null) { + ssiExternalResolver.log("SSI processing stopped", e); + } } return lastModifiedDate; } diff --git a/test/org/apache/catalina/ssi/TestSsiServlet.java b/test/org/apache/catalina/ssi/TestSsiServlet.java index d1363782c3..3599530415 100644 --- a/test/org/apache/catalina/ssi/TestSsiServlet.java +++ b/test/org/apache/catalina/ssi/TestSsiServlet.java @@ -117,4 +117,41 @@ public class TestSsiServlet extends TomcatBaseTest { } Assert.assertNotNull(lastModified); } + + + @Test + public void testBadConditionalExpressionReportsError() throws Exception { + // A conditional directive with an expression that cannot be parsed + // stops processing of the document, but it must report the default + // error message rather than truncating the response silently. + Tomcat tomcat = getTomcatInstance(); + + File appDir = new File(getTemporaryDirectory(), "ssi-badif"); + Assert.assertTrue(appDir.mkdirs() || appDir.isDirectory()); + addDeleteOnTearDown(appDir); + File doc = new File(appDir, "badif.shtml"); + try (Writer writer = new OutputStreamWriter(new FileOutputStream(doc), StandardCharsets.ISO_8859_1)) { + writer.write("BEFORE-CONTENT\n"); + writer.write("<!--#if expr=\"this is not valid (\"-->\n"); + writer.write("AFTER-CONTENT\n"); + } + + Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); + Tomcat.addServlet(ctxt, "ssi", new SSIServlet()); + ctxt.addServletMapping("*.shtml", "ssi"); + + tomcat.start(); + + Map<String,List<String>> resHeaders = new HashMap<>(); + String path = "http://localhost:" + getPort() + "/badif.shtml"; + ByteChunk out = new ByteChunk(); + + int rc = getUrl(path, out, resHeaders); + Assert.assertEquals(HttpServletResponse.SC_OK, rc); + String body = new String(out.getBuffer(), 0, out.getLength(), StandardCharsets.ISO_8859_1); + Assert.assertTrue(body.contains("BEFORE-CONTENT")); + Assert.assertTrue(body.contains("[an error occurred")); + // Processing of the remainder of the document stops at the failing directive + Assert.assertFalse(body.contains("AFTER-CONTENT")); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
