DEV Community

James Moberg
James Moberg

Posted on

Undocumented Change to ColdFusion 2021 CFHTMLHead & CFContent

According to my unit tests, after ColdFusion 2018.0.0-15, Adobe changed the way that CFHTMLHead works with CFContent. Prior to CF2021, any strings that were added to the header buffer via CFHTMLHead was outputted to the HTML HEAD section (or top of the page if you neglected to include a HEAD section) on onRequestEnd even if a CFContent (with or without reset) was performed.

After upgrading to CF2021.0.0 (or even latest v5), I've discovered that the header buffer is cleared whenever performing CFContent. I've compared this behavior with latest versions of CF2018 and Lucee (and even CF2016) and they all faithfully continue to retain & output the header buffer.

I'm not sure why Adobe changed this behavior. It's possible that it's either a bug or they intentionally changed it (for security reasons) and then neglected to document it.

Our framework doesn't use CFHTMLHead until the very end of the request process. If it's used in multiple places throughout the life of a web request, it can cause some havoc. Ben Nadel warned about potential issues in his Thoroughly Document Your Use Of ColdFusion's CFHTMLHead Tag blog article back in 2009.

Our approach, since CF8/9, was to create two (2) request-scoped array variables: request.extraHtml and request.extraOnReady. (Using the request scope allows us to easily inject JS/CSS/metatags/etc from any CFTags, UDFs, CFC, etc.) When we want to add anything to the HTML Head section (or jQuery's onReady method), we append it to these arrays. During the "end of the page" request, we flatten these arrays to strings, audit them to determine which dependencies should be injected and then add the final content payload to the head section.

This "append-and-output-later" approach has worked well for us with our internal homegrown framework, but not so well for other developers that indiscriminately use CFHTMLHead and publicly reference different CFM files in URLs with different entries & random exits points.

Another developer (who uses our jsoupUtil library... to be blogged about later) reported an issue where they were experiencing JavaScript errors on their webpage. We checked the HTML source and determined that JavaScript files weren't even being included in the source code. It was immediately obvious to us that the unsafe, inline onclick JavaScript events were failing because dependencies weren't being referenced... but why? We also had the entire codebase hosted on a staging environment and it worked there, but not in production. The only difference we could determine was that the staging environment used CF2016 whereas development used CF2021.

I was able to verify this "nuance" using two (2) lines of CFML.

<cfhtmlhead text="Hello there #now()#">
<cfcontent type="text/html; charset=UTF-8"> 
Enter fullscreen mode Exit fullscreen mode

You can copy-and-paste the above code and try it at TryCF.com. (Sorry. I'd provide a URL with the GIST, but TryCF never refreshes the content... so subsequent GIST updates never get reflected.) You can also try this at CFFiddle. ACF2021 outputs nothing while CF2018 does what every other non/pre-CF2021 version of a CFML renderer would do... output the header buffer at the end of the request (er, that is if the buffer isn't already flushed, otherwise it throws a CF error).

While researching for a solution to identify the header buffer output, I came across a comment from David Boyer in Ben Nadel's blog post regarding a solution that he wrote for ColdFusion 7 (13 years ago). He posted two (2) UDFs to pastebin to "undo anything that CFHtmlHead intends to do" (ie, "clear") and also to "retrieve anything that CFHtmlHead intends to do (ie, "get").

I've combined these functions into a single function with a "get-with-a-clean" option. (Too much? Nah... I love adding options to UDFs and most of the code between the two was the identical.) (NOTE: I like that he created a single local struct for the variables. I previously used a VARed temp variable. This approach makes it a lot easier to troubleshoot by dumping a single object that contains all variables.)

To integrate this getCFHtmlHead() UDF, I recommend scanning your codebase for usage of CFContent (where HTML content is being returned versus downloading a binary file), get the header string prior to CFContent and then re-adding them via CFHTMLHead after CFContent.

<cfhtmlhead text="Hello there #now()#">
<cfset headerContent = getCFHtmlHead()>
<cfcontent type="text/html; charset=UTF-8"><p>Hi. I'm outputted content. (Hopefully I'm a valid HTML document with a HEAD section. If not, all headers will be prepended to the beginning out the output buffer.)</p>
<cfif len(headerContent)>
    <cfhtmlhead text="#headerContent#">
</cfif>
Enter fullscreen mode Exit fullscreen mode

NOTE: I noticed that CFHTMLHead outputs everything as a single string without any carriage returns. This means that if you want your code to look nicer, you'll need to append line feed (chr(13)) and/or carriage return (chr(10)) characters to the end of the values that you add via CFHTMLHead. We prefer to avoid using CFHTMLHead and instead manually append our headers (at the end of the request) and then use jsoup to remove whitespace & minimally indent code.

[Update 2022-10-30] I've reported it to Adobe as a bug here CF-4215634.

Source Code

Here's the source code to the getCFHtmlHead UDF. Enjoy!

Top comments (4)

Collapse
 
bennadel profile image
Ben Nadel

Very interesting. I've always thought of CFContent as a tag that resets the buffer and essentially "takes over" the request management. I wonder if Adobe is treating it as a "bug" that you could previously mix the CFContent tag with the CFHtmlHead tag. Curious to see what they say in the ticket you filed.

Collapse
 
gamesover profile image
James Moberg

I just cleaned up some CFML (for a web application I didn't write, but am contracted to occassionally troubleshoot) and CFContent was used on 23 different templates. In cases where a PDF was being downloaded, it was easy to ignore... but ~10 cases had non-binary text and/or XML returned (for psuedo API or RSS, I guess). I intentionally added the getCFHtmlHead function to clear the head buffer so that any existing content wouldn't be prepended to the output and invalidate it on pre-CF2021 servers.

It's possible that this code currently works well in their CF2021 environment (since the head buffer is cleared by default now), but they indicated that they also host the same code on CF2018. I've updated their code so that it will now work consistently in both places. (phew) Now I only need to audit our 25 years worth of projects (from CF3-2021) to determine our "cross-version" compatibility going forward.

Collapse
 
gamesover profile image
James Moberg • Edited

Same here... I never know how Adobe is going to respond. If they intentionalyl did this, they need to update the documentation on both tags to indicate that "CFContent clears the header buffer" so that it's not a surprise.

A good approch, IMHO, would be to add a new param so that they don't break pre-existing functionality. (That's what they'd normally do, right?)

If not reverted to original behavior and a feature flag needs to be added, I'd prefer this it not be a single server or application-wide setting since uses of CFContent can vary throughout an application. When returning an application-based "bad request" message, I may want to eliminate the header buffer entirely, but that would depend on whether that's a simple message-only page or it also displays the faully branded website (ie, w/CSS or JS added to the header buffer to support things like a navigational menu.)

Collapse
 
bennadel profile image
Ben Nadel

I definitely prefer when there is an Application-specific setting (in general) so that I can have all the configuration in the code; and not have to worry about dealing with the Admin.