Monday 26 August 2013

The process of creating a clear repro case for a bug: <cfchart> vs Async CFML gateways

G'day:
It's bank holiday Monday and it's gloriously sunny outside, but for some reason I am investigating a bug I didn't even know existed nor would ever be likely to encounter. I'm a dick.

I ran across this thread today on the Adobe ColdFusion forums: "CF10: cfchart in a gateway giving: java.lang.NullPointerException". I looked at it not because I have any particular interest in <cfchart>, but because I was intrigued how charts and gateways fit together. Especially given I have no real exposure to either.

So it seems someone has some code that generates a <cfchart>, and that works fine. But if the code is called via a gateway (by which I presumed they meant an event gateway (in this case an async ColdFusion one). They didn't offer a great deal of info, and certainly not enough code to demonstrate "this version works"; "this version doesn't" (indeed the code they did provide wasn't even syntactically correct). So I set about reproducing it.

Firstly I set about creating a control test: fixing their code to work with minimum adjustment, then set about proving it could work in the normal sequence of events (just without the async gateway being involved).

<!--- Chart.cfc --->
<cfcomponent>
    
    <cffunction name="generateChart" access="public" returntype="binary">
        <cfchart name="cfChartData" format="png" chartWidth="650" chartHeight="200" rotated="yes" show3d="false">
            <cfchartseries type="bar">
                <cfchartdata item="item 1" value="#randRange(1,10)*5#">   
                <cfchartdata item="item 2" value="#randRange(1,10)*5#">   
                <cfchartdata item="item 3" value="#randRange(1,10)*5#">   
                <cfchartdata item="item 4" value="#randRange(1,10)*5#">                           
            </cfchartseries>
        </cfchart>
        <cfreturn cfChartData>          
    </cffunction>
    
    <cffunction name="generateChartProxy" access="remote" returntype="binary">
        <cfset var chart = generateChart()>
        <cfcontent variable="#toBinary(toBase64(chart))#" type="image/png" reset="true">
    </cffunction>
    
    <cffunction name="writeChartToFile" access="public" returntype="void">
        <cfargument name="fileName" type="string" required="true">
        <cffile action="write" output="#generateChart()#" file="C:\temp\#arguments.fileName#.png">
    </cffunction>
    
</cfcomponent>

Apologies for the all-tag CFC - I know it's a bit of an eyesore - but I need to do a <cfchart> in there, so I had little choice.

And I call generateChart() via this code:

<!--- callDirect.cfm --->
<cfimage action="writeToBrowser" source="#new Chart().generateChart()#">

And this displays the chart on the screen. A tick goes in the box: one can call a function which generates a chart, then use the resulting image data to populate an <img> tag.

As a side test I also wrapped that function in a proxy so I could call it from the URL and have it return an image, eg: browsing to Chart.cfc?method=generateChartProxy results in a chart being displayed. OK, so it works.

However if I'm going to be calling this code via an async proxy, I won't be able to test by putting the thing on the screen, as the code won't be being executed in a browser-initiated request, so I instead wrote a wrapper function which just writes the image to disk. I tested this function via a browser request too:

<!--- testWriteChartToFile.cfm --->
<cfset fileName = createUuid()>
<cfset new Chart().writeChartToFile(fileName)>
<cfoutput><img src="#fileName#.png"></cfoutput>

This worked fine, so I am now certain my house is in order, and I have a working baseline test. Now to call it via an asynchronous CFML gateway.

I haven't used one of these for years, so I needed to RTFM to work out what I needed to do. Basically I need to create a CFC which implements a method onIncomingMessage() which takes an argument which holds the event that caused it to fire, but some data:

// AsyncProxy.cfc
component {

    void function onIncomingMessage(required struct event){
        try {
            new Chart().writeChartToFile(event.data.fileName);
        }
        catch (any e){
            var debugInfo = "";
            savecontent variable="debugInfo" {
                writeDump(local);
            }

            var ts = timeFormat(now(),'HHmmss');
            var fileDirectory = "C:\temp\";
            var filePath = fileDirectory & "errorDump_#ts#.html"; 
            fileWrite(filePath, debugInfo);

            rethrow;
        }
    }
    
}

This basically takes a file name, and generates the chart, saving it in the specified file. I have some debugging in there too.

To be pedantic about things, I even test a direct call to this:

new AsyncProxy().onIncomingMessage({data={fileName="foo"}});

This correctly saves C:\temp\foo.png. So... err... all the code works. I think we've established that now.

So I now call it via the async CFML gateway I set up (you can just RTFM for that, in your own time):

sendGatewayMessage("AsyncCfml", {fileName=timeFormat(now(), "HHmmss")});

And this... doesn't work. Instead of an image file, I get an error dumped:

struct
ARGUMENTS
struct
EVENT
struct
DATA
struct
FILENAME131219
GATEWAYIDAsyncCfml
GATEWAYTYPECFMLGateway
ORIGINATORIDCFMLGateway
DEBUGINFO[empty string]
THIS
component AsyncProxy
METHODS
e
struct
Message[empty string]
StackTracejava.lang.NullPointerException at coldfusion.tagext.html.ajax.AjaxRBFileMap.getRBFileName(AjaxRBFileMap.java:66) at coldfusion.tagext.html.ajax.AjaxRBFileMap.importResourceBundleJS(AjaxRBFileMap.java:52) at coldfusion.tagext.html.ajax.HtmlAssembler.importJS(HtmlAssembler.java:958) at coldfusion.tagext.html.ajax.HtmlAssembler.importJS(HtmlAssembler.java:854) at coldfusion.tagext.html.ajax.HtmlAssembler.importJS(HtmlAssembler.java:820) at coldfusion.tagext.io.ChartTag.doStartTag(ChartTag.java:1078) at cfChart2ecfc827651515$funcGENERATECHART.runFunction(C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc:5) [...]  at cfChart2ecfc827651515$funcWRITECHARTTOFILE.runFunction(C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc:23) [...] at cfAsyncProxy2ecfc1874695918$funcONINCOMINGMESSAGE.runFunction(
C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\AsyncProxy.cfc:5)
Suppressed
array [empty]
TagContext
array
1
struct
COLUMN0
IDCFCHART
LINE5
RAW_TRACEat cfChart2ecfc827651515$funcGENERATECHART.runFunction(
C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc:5)
TEMPLATEC:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc
TYPECFML
2
struct
COLUMN0
IDCF_UDFMETHOD
LINE23
RAW_TRACEat cfChart2ecfc827651515$funcWRITECHARTTOFILE.runFunction(
C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc:23)
TEMPLATEC:\webroots\shared\git\blogExamples\bugs\chartViaGateway\Chart.cfc
TYPECFML
3
struct
COLUMN0
IDCF_TEMPLATEPROXY
LINE5
RAW_TRACEat cfAsyncProxy2ecfc1874695918$funcONINCOMINGMESSAGE.runFunction(
C:\webroots\shared\git\blogExamples\bugs\chartViaGateway\AsyncProxy.cfc:5)
TEMPLATEC:\webroots\shared\git\blogExamples\bugs\chartViaGateway\AsyncProxy.cfc
TYPECFML
Typejava.lang.NullPointerException

Which is the same error as mentioned in the forum thread.

I then ran this code on ColdFusion 9.0.2, and it ran fine. So it's a regression in CF10.

The forum post mentions an already-raised bug, but it doesn't really distill the situation down very well, and clutters the situation up with <cfpresentation> and calling CFCs from within Java etc. None of which really have anything to do with the issue. However it is the same issue.

I'll update the ticket with my notes here.

I found another issue with the async gateway when working on this... in that expandPath() doesn't always work. I'll write that up later but my will to live has been sapped, and I also haven't yet locked down exactly what the repro case for that is.

I think now I shall read a book.

--
Adam

PS: the code for all this is in my github repo.