Undocumented Change to ColdFusion 2021 CFHTMLHead & CFContent
James Moberg
Posted on October 30, 2022
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">
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>
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!
Posted on October 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 8, 2022