[MAGNOLIA-8738] Weird exception in freemarker when rendering empty string from expression (${""}) Created: 02/Feb/23  Updated: 03/Feb/23

Status: Open
Project: Magnolia
Component/s: None
Affects Version/s: 6.2.26
Fix Version/s: None

Type: Bug Priority: Neutral
Reporter: Samuli Saarinen Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Template:
Acceptance criteria:
Empty
Task DoD:
[ ]* Doc/release notes changes? Comment present?
[ ]* Downstream builds green?
[ ]* Solution information and context easily available?
[ ]* Tests
[ ]* FixVersion filled and not yet released
[ ]  Architecture Decision Record (ADR)
Bug DoR:
[ ]* Steps to reproduce, expected, and actual results filled
[ ]* Affected version filled

 Description   

Steps to reproduce

  1. Have freemarker component that renders empty expression (${""})
  2. Render the page causes following stacktrace

 

Caused by: java.lang.StringIndexOutOfBoundsException: endIndex must be valid
    at org.apache.commons.text.TextStringBuilder.append(TextStringBuilder.java:548) ~[commons-text-1.10.0.jar:1.10.0]
    at org.apache.commons.text.TextStringBuilder.append(TextStringBuilder.java:77) ~[commons-text-1.10.0.jar:1.10.0]
    at info.magnolia.rendering.util.AppendableWriter.write(AppendableWriter.java:66) ~[classes/:?]
    at java.io.Writer.write(Writer.java:290) ~[?:?]
    at java.io.Writer.write(Writer.java:249) ~[?:?]
    at java.io.Writer.append(Writer.java:322) ~[?:?]
    at java.io.Writer.append(Writer.java:366) ~[?:?]
    at java.io.Writer.append(Writer.java:51) ~[?:?]
    at info.magnolia.rendering.util.AppendableWriter.write(AppendableWriter.java:66) ~[classes/:?]
    at java.io.Writer.write(Writer.java:290) ~[?:?]
    at java.io.Writer.write(Writer.java:249) ~[?:?]
    at freemarker.core.DollarVariable.accept(DollarVariable.java:70) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:347) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:389) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacroOrFunctionCommonPart(Environment.java:889) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacro(Environment.java:825) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.UnifiedCall.accept(UnifiedCall.java:84) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:383) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeNestedContent(Environment.java:633) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.BodyInstruction.accept(BodyInstruction.java:60) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:383) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacroOrFunctionCommonPart(Environment.java:889) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacro(Environment.java:825) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.UnifiedCall.accept(UnifiedCall.java:84) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:347) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:389) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacroOrFunctionCommonPart(Environment.java:889) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.invokeMacro(Environment.java:825) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.UnifiedCall.accept(UnifiedCall.java:84) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:347) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:353) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.visit(Environment.java:353) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.core.Environment.process(Environment.java:326) ~[freemarker-2.3.31.jar:2.3.31]
    at freemarker.template.Template.process(Template.java:383) ~[freemarker-2.3.31.jar:2.3.31]
    at info.magnolia.freemarker.FreemarkerHelper.render(FreemarkerHelper.java:174) ~[magnolia-freemarker-support-6.2.26.jar:?]
    at info.magnolia.rendering.renderer.FreemarkerRenderer.onRender(FreemarkerRenderer.java:100) ~[magnolia-rendering-6.2.26.jar:?]
    at info.magnolia.rendering.renderer.AbstractRenderer.render(AbstractRenderer.java:166) ~[magnolia-rendering-6.2.26.jar:?]
    at jdk.internal.reflect.GeneratedMethodAccessor405.invoke(Unknown Source) ~[?:?]
    at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
    at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]
    at info.magnolia.config.source.DefinitionProviderWrapperWithProxyFallback$DirectDelegator.interceptWithReturnValue(DefinitionProviderWrapperWithProxyFallback.java:151) ~[magnolia-configuration-6.2.26.jar:?]
    at info.magnolia.rendering.renderer.FreemarkerRenderer$ByteBuddy$wmpCBbn3.render(Unknown Source) ~[?:?]
    at info.magnolia.rendering.engine.DefaultRenderingEngine.render(DefaultRenderingEngine.java:120) ~[magnolia-rendering-6.2.26.jar:?]
    at info.magnolia.rendering.engine.DefaultRenderingEngine.render(DefaultRenderingEngine.java:101) ~[magnolia-rendering-6.2.26.jar:?]
    at info.magnolia.rendering.engine.DefaultRenderingEngine.render(DefaultRenderingEngine.java:96) ~[magnolia-rendering-6.2.26.jar:?]
    at info.magnolia.rendering.engine.DefaultRenderingEngine$$EnhancerByCGLIB$$704b1c97.render(<generated>) ~[magnolia-rendering-6.2.26.jar:?]
    at info.magnolia.templating.elements.ComponentElement.begin(ComponentElement.java:122) ~[magnolia-templating-6.2.26.jar:?]
    at info.magnolia.templating.renderers.NoScriptRenderer.onRender(NoScriptRenderer.java:103) ~[magnolia-templating-6.2.26.jar:?]

 

 

This is happening in some special situation when freemarker output is written to TextStringBuilder which might not always be the case as this was happening on some of our components and I could not reproduce it every where (like page template or some other component for example)

I also don't know what has changed that started causing this as the components that started showing the error have been in use for several years.

 

As a workraround it seems that it is enough to render e.g. space (${" "}) or if I wrap the component in  'attempt' block that probably changes the "sink" where output is written.

Also overriding the info.magnolia.rendering.util.AppendableWriter#write with something like this seems to fix the issue

@Override
public void write(char[] chars, int start, int end) throws IOException {
    if(end > 0) {
        appendable.append(new String(chars), start, end);
    } 
} 

 

Expected results

No exception

 



 Comments   
Comment by Samuli Saarinen [ 03/Feb/23 ]

I dig this a bit deeper and found out that the special case was in our code base that was rendering parts of web page content to a string using TextStringBuilder from commons-io.

I could resolve the situation using java.lang.StringBuilder as the appendable. But I still think that your AppendableWriter could be fixed as well and for a reference you can look for the implementation in e.g. Guava that behaves correctly: https://github.com/google/guava/blob/master/guava/src/com/google/common/io/AppendableWriter.java#L58

Here is my test set that I used

import info.magnolia.rendering.util.AppendableWriter;
import org.junit.Test;

import java.io.IOException;

public class MagnoliaAppendableWriterTest {

    @Test
    public void writeEmptyWithCommonsTextStringBuilder() throws Exception {
        AppendableWriter w1 = new AppendableWriter(new org.apache.commons.text.TextStringBuilder());
        w1.write("");
        // fail
    }

    @Test
    public void writeEmptyWithStringBuilder() throws Exception {
        AppendableWriter w1 = new AppendableWriter(new StringBuilder());
        w1.write("");
        // pass
    }


    @Test
    public void writeEmptyWithCommonsTextStringBuilderWithGuavaAppendableWriter() throws Exception {
        GuavaAppendableWriter w3 = new GuavaAppendableWriter(new org.apache.commons.text.TextStringBuilder());
        w3.write("");
        //pass
    }

    public static class GuavaAppendableWriter extends AppendableWriter {

        private final Appendable appendable;

        public GuavaAppendableWriter(Appendable appendable) {
            super(appendable);
            this.appendable = appendable;
        }

        @Override
        public void write(char[] chars, int start, int end) throws IOException {
            // from https://github.com/google/guava/blob/master/guava/src/com/google/common/io/AppendableWriter.java#L58
            appendable.append(new String(chars, start, end));
        }
    }
}
Generated at Mon Feb 12 04:35:21 CET 2024 using Jira 9.4.2#940002-sha1:46d1a51de284217efdcb32434eab47a99af2938b.