Remote Function Content-Type difference between Lucee and ColdFusion

Not sure if this is more appropriate as a Language question or Dev question but I’ll try here.

I found an issue while trying to port some websites from CF to Lucee with regards to Content-Type headers in remote function calls. I’ve confirmed identical behavior that ColdFusion functions identically with both CF 11 and CF 2021 but Lucee behaves differently. For Lucee, I tested with an installed version on Windows 2022 server (IIS) and with Commandbox locally on my dev machine (the results are consistent with both).

I’m aware that arrays are passed by reference instead of value but this does not appear to be what is happening as this same result is true of strings, arrays, and structs.

component output="false" {
	remote function testArray() returnFormat="json" {
		return [0, 1, 2, 3];
	}

	remote function testStruct() returnFormat="json" {
		return {v0=0, v1=1, v2=2, v3=3};
	}

	remote function testString() returnFormat="json" {
		return "0, 1, 2, 3";
	}
}

Given a CFC as shown above running in Lucee 6.0.1 and also in ColdFusion 11 or 2021 (two I tested), I get different Content-Type responses for the functions.

Lucee Content-Type response to all 3 of the functions above:

application/json;charset=UTF-8

ColdFusion Content-Type response to all 3 of the functions above:

text/html;charset=UTF-8

I am not sure if there’s a global setting somewhere that could affect the output but if my ajax calls suddenly begin passing strings as objects, I’d have to hunt all over my code to find and fix type conversions. And that could be quite arduous. I’m hoping there’s a simple solution to explain WHY and to hopefully fix. But any ideas on what’s happening or a simple way to fix without having to individually analyze 2,000+ functions in each of the projects I’m porting over?

I’m assuming this is restricted to “remote” function calls but I suppose it may not necessarily be so depending on the exact cause.

Hi @jjblodg. Wouldn’t you need to specify a returnFormat of ‘plain’ if you want a string to be returned rather than JSON?

It looks like Lucee is doing what is being asked of it here.

@martin Thanks for the response. I agree that it logically makes sense to be application/json. And I’ve always wondered about the returnFormat=“json” when consuming it because it didn’t make sense. Both are actually returning the content AS json with regards to the format (valid array data is being passed back for the particular function that triggered this issue for me). However, just the Content-Type is different.

There is a definite difference in the way Lucee and ACF are handling the response. I think I’d say that Lucee is probably doing it the more logical way. Unfortunately, ACF has been doing it a different way and all of our javascript / script interactions are generally based on the way ACF has been doing it for decades (albeit a potentially incorrect way).

After doing a little more search on the CF side rather than Lucee, it looks like others experience the same thing and have been finding code workarounds for a while (basically forever I guess):

So at this point, I may have to play around with global CFCONTENT statements under certain situations to try and reproduce the “illogical” behavior from ACF. Otherwise, I have to make hundreds or thousands of code changes across my code to correct for the difference in mime type. I’m in the process of migrating a physical environment to the cloud and was hoping to make the switch from ACF to Lucee in the process. But I can’t do so if there are months of dev work involved in the switch (not Lucee’s fault, but just differences between Lucee and CF).

I would however, suggest that even though the way Lucee is handling the output makes sense, I think it might be worth mentioning in the Differences section of the Docs somewhere (Lucee Language and Syntax Differences?).

Mostly having a monologue here but I’ll keep adding to this in case anyone else can benefit. :slight_smile:

So I’ve written something in application.cfc to try to address this based on some internal code and patterned somewhat after Ben Nadel’s post on the subject (https://www.bennadel.com/blog/1647-learning-coldfusion-9-application-cfc-oncfcrequest-event-handler-for-cfc-requests.htm).

	/* @hint Fires after pre cfc processing is complete */
	public void function OnCFCRequest(required string cfcName, required string method, required struct args)
	output="true" {
		// Doing this in case it wasn't created during app start (ie code added after app started?)
		if (!application.keyExists('cfc_cache')) {
			application.cfc_cache = {};
		}

		// Populate cache data for this request if needed
		if (!application.cfc_cache.keyExists(arguments.cfcName)) {
			application.cfc_cache[arguments.cfcName] = {
				component = createObject("component", arguments.cfcName),
				meta = {
					returnType = ""
				}
			};

			application.cfc_cache[arguments.cfcName].meta.append(
				getMetaData(application.cfc_cache[arguments.cfcName].component[arguments.method]),
				true
			);
		}

		var requestCFC = application.cfc_cache[arguments.cfcName].component;
		var methodMeta = application.cfc_cache[arguments.cfcName].meta;

		var cfcResponse = invoke(requestCFC, arguments.method, arguments.args);

		var response = {
			data = "",
			mimetype = "text/plain"
		};

		if ((methodMeta.returnType != "void") && !IsNull(cfcResponse)) {
			var returnFormat = methodMeta.keyExists("returnFormat") ? methodMeta.returnFormat : "plain";

			// Define final returnFormat
			param name="url.returnFormat", type="string", default=returnFormat;
			param name="url.fallbackToACF", type="boolean", default=true;

			if (url.returnFormat == "json") {
				response.mimetype = url.fallbackToACF ? "text/html" : "application/json";
				response.data = isSimpleValue(cfcResponse) ? cfcResponse : serializejson(cfcResponse);
			} else if (methodMeta.returnType == "xml") {
				response.mimetype = "application/xml";
				response.data = ToString(cfcResponse);
			} else if (url.returnFormat == "wddx") {
				response.mimetype = "application/xml";
				cfwddx(action="cfml2wddx", input="#cfcResponse#", output="response.data");
			} else {
				response.data = cfcResponse;
			}
		}

		// Convert to binary for cfcontent output
		var binaryResponse = toBinary(toBase64(response.data));

		cfheader(name="content-length", value="#arrayLen(binaryResponse)#");
		cfcontent(type="#response.mimetype#", variable="#binaryResponse#");
	}

With this code, I can successfully get CF to behave like Lucee where it will adjust the mime type of json to application/json instead of text/html. It works great in ACF (although I don’t really want to implement this in CF). I can toggle url.fallbackToACF on / off in CF and it toggles the output between json / text.

Lucee will correctly adjust to behave like CF if url.fallbackToACF is true but barfs when it’s false. No matter what I’m setting in headers, it sends an output response header of

Return-Format: wddx

This causes the request to fail in the browser because it’s expecting output to be XML.

Maybe this is more expected behavior from Lucee but it seems like a serialized object should be able to be sent as “text/html” without having to set things to wddx. I don’t know if I’m missing something obvious here but this one seems like Lucee should be able to suppress the default wddx behavior in certain scenarios.

On the positive side, I can try to test my code with url.fallbackToACF set to true and see if that allows me to carry on with CF-like behavior with this issue. That’s ultimately what I need short-term anyway. But it would be nice to know if I could turn this off at some point without having everything sent as wddx.

2 Likes

Great that you are on the way to a solution for your problem.

I tried this code myself out of interest, but everything seemed to have the expected headers in Lucee (v5.4.3.16) - I was not getting any wddx unless I set the returnFormat to wddx. In fact I cannot see a Return-Format header at all?

On a side, I found it interesting that if you set the returnFormat to xml, it results in the returnFormat key being excluded from the methods metaData entirely! I presume this is why you use returnType when trying to determine an XML output?

Digging into the what I think is the source code for this, it looks like the getMetaData function for a component is missing XML as a format:

@martin Interesting. I relaunched command box under 5.4.3.16 and tried it again. I got the same results as you. I didn’t end up with the extra wddx header so I think it’s something specific with version 6 that doesn’t happen in 5.

As for my method of using onCFCRequest to address my issue, I found out that while it mostly works, it has issues when I have to serializeJSON() structs. That action ends up converting the case of some/any structs from known case to all upper case despite whatever setting is in Lucee admin for struct handling. I’m pretty sure that setting isn’t meant to apply to serializeJSON() though so don’t think it’s an issue to be solved. However, that change means that any JS code that was relying on variables being a certain case would now be broken. So I can’t globally “correct” my code with the new handler and would have to update all of my JS code anyway.

I may research a little more to see how much effort it would be to just change JS and fall back to default Lucee behavior of app/json instead of text/html. But if I hit more roadblocks after this, I’m afraid I may have to abandon Lucee and go back to ACF as it’s no longer a “quick port” to migrate my systems over to Lucee. It would be unfortunate to miss on cost savings and configuration possibilities but it’s not as simple of a port as I was hoping (at least with my code).

A continuation of my blog with a better solution… LOL

Due to my issues with struct handling in the OnCFCRequest method, I thought I’d try a slightly different approach. Basically, I ignored onCFCRequest() and made use of onRequestEnd() instead. It’s similar to the other approach with the added benefit that I don’t have to interrupt the calls and inject my own handler for the content. It can do whatever it was doing before and now I can simply change the Content-Type header in certain circumstances.

Here’s the new method that seems to be doing exactly what I need so far in basic testing.

Application.cfc in OnRequest():

if (ListLast(cgi.script_name, ".") == "cfc") {
	contentTypeHandler();
}

And then I added this function in Application.cfc to handle the header conversions. I’m sure it can be optimized, extended, etc but it’s functional right now and that’s the most important thing for me at the moment.

/* @hint Sets headers in certain scenarios to mimic ColdFusion behavior */
private void function contentTypeHandler()
output="false" {
	param name="url.fallbackToACF", type="boolean", default=true;
	param name="request.fallbackToACF", type="boolean", default=true;

	if (!request.fallbackToACF) {
		// Using request as priority in case we want to FORCE a specific content-type internally
		return;
	} else if (!url.fallbackToACF) {
		// Otherwise, if the caller wants to adjust the ouput, allow them to do that here
		return;
	}

	// Get cfc name & path in dot-notation formation
	var cfcName = cgi.SCRIPT_NAME;

	// Drop .cfc from path and change to dots
	cfcName = Reverse(ListRest(Reverse(cfcName), "."));
	cfcName = listChangeDelims(cfcName, ".", "\/");

	// Get method name from url params
	var method = "";

	for (item in ListToArray(cgi.query_string, "&")) {
		var data = ListToArray(item, "=");

		if (data.len() == 2) {
			if (data[1] == "method") {
				method = data[2];
				break;
			}
		}
	}

	if (!Len(method)) {
		// No method discovered so carry on as-is
		return;
	}

	// Doing this in case it wasn't created during app start (ie code added after app started?)
	if (!application.keyExists('cfc_cache')) {
		application.cfc_cache = {};
	}

	// Populate cache data for this request if needed
	if (!application.cfc_cache.keyExists(cfcName)) {
		try {
			application.cfc_cache[cfcName] = {
				component = createObject("component", cfcName),
				meta = {
					returnFormat = ""
				}
			};
		} catch (any e) {
			// This could happen if the requested template doesn't exist or is just broken for some reason
			return;
		}

		// Now for the part we really need: metadata
		application.cfc_cache[cfcName].meta.append(
			getMetaData(application.cfc_cache[cfcName].component[method]),
			true
		);
	}

	// For now, just modifying json output but might extend or adjuist later as needed
	if (application.cfc_cache[cfcName].meta.returnFormat == "json") {
		cfheader(name="Content-Type", value="text/html");
	}
}

Now I can mimic the (likely incorrect) ColdFusion behavior that sends returnFormat=“json” content back to the call as “text/html”. But I’m also able to override this behavior as needed through either url or request variables (in case I want to force “correct” behavior). Thus far, it’s working splendidly and carry on with my other testing without having to change json or js behaviors to compensate for the difference between Lucee and ColdFusion. So I’m much more optimistic that this may be achievable.

1 Like