ColdFusion 10 - Control Flow And Scopes During A WebSocket Request
A few years ago, I demonstrated that the FORM scope does not exist during SOAP-based web service calls. SOAP requests are handled somewhat outside of the normal ColdFusion page request model. In ColdFusion 10, we now have native WebSocket calls. These, too, are handled outside of the normal ColdFusion page request model. So, this got me thinking - are there any scope-existence oddities that exist during a ColdFusion WebSocket page request?
NOTE: At the time of this writing, ColdFusion 10 was in public beta.
To test this, I created a custom event handler, onWSRequestStart(), in my Application.cfc ColdFusion framework component to initialize incoming WebSocket requests. Then, within this event handler, I check for the existence of a number of ColdFusion scopes; and, if they exist, I CFDump() them to a file:
Application.cfc - Our ColdFusion Application Framework Component
<cfscript>
// NOTE: CFScript tags added for Gist color-coding. Remove.
component
output="true"
hint="I define the application settings and event handlers."
{
// Define the application settings.
this.name = hash( getCurrentTemplatePath() );
this.applicationTimeout = createTimeSpan( 0, 0, 20, 0 );
this.sessionManagement = true;
this.sessionTimeout = createTimeSpan( 0, 0, 20, 0 );
// Set up the WebSocket channels.
this.wsChannels = [
{
name: "chat",
cfcListener: "WSApplication"
}
];
// Log the application request.
logData( "APPLICATION instantiated #now()#" );
// I initialize the incoming WebSocket request. In this case
// we're just gonna run through a number of scopes and data
// points to see if they exist during a WebSocket request.
function onWSRequestStart( type, channel, user ){
// Check for APPLICATION.
if (isDefined( "application" )){
logData( "APPLICATION:" );
logData( application );
} else {
logData( "APPLICATION is NULL" );
}
// Check for SESSION.
if (isDefined( "session" )){
logData( "SESSION:" );
logData( session );
} else {
logData( "SESSION is NULL" );
}
// Check for CGI.
if (isDefined( "cgi" )){
logData( "CGI:" );
logData( cgi );
} else {
logData( "CGI is NULL" );
}
// Check for URL.
if (isDefined( "url" )){
logData( "URL:" );
logData( url );
} else {
logData( "URL is NULL" );
}
// Check for FORM.
if (isDefined( "form" )){
logData( "FORM:" );
logData( form );
} else {
logData( "FORM is NULL" );
}
// Check for REQUEST.
if (isDefined( "request" )){
logData( "REQUEST:" );
logData( request );
} else {
logData( "REQUEST is NULL" );
}
// Check for COOKIE
if (isDefined( "cookie" )){
logData( "COOKIE:" );
logData( cookie );
} else {
logData( "COOKIE is NULL" );
}
// Try to get the HTTP request data.
try {
logData( "HTTP Request Data:" );
logData( getHttpRequestData() );
} catch( Any error ){
logData( "getHttpRequestData() Failed" );
}
// Return true so the request will be processed.
return( true );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// I log the arguments to the text file for debugging.
function logData( data ){
// Create a log file path for debugging.
var logFilePath = (
getDirectoryFromPath( getCurrentTemplatePath() ) &
"log.txt"
);
// Dump to TXT file.
writeDump( var=data, output=logFilePath );
}
}
// NOTE: CFScript tags added for Gist color-coding. Remove.
</cfscript>
As you can see, I am logging the actual Application.cfc instantiation in addition to the ColdFusion scopes. I did this because I have seen some odd caching issues with the WebSocket requests. In fact, even with this test, I found that my Application.cfc file started caching randomly. Even though the Application.cfc is instantiated for each request (which you'll see in the output), the actual "definition" of the Application.cfc is cached in the template cache.
To make sure that your Application.cfc component receives the most updated code, you can manually clear the "Template Cache" in the ColdFusion Administrator.
If you look at my Application.cfc settings, you'll see that I defined one WebSocket channel listener - WSApplication.cfc. This is the component that actually gets invoked during a WebSocket request. I have set it up so that it, in turn, instantiates the Application.cfc component and invokes the onWSRequestStart() event handler.
WSApplication.cfc - Our WebSocket Channel Listener
<cfscript>
// NOTE: CFScript tags added for Gist color-coding. Remove.
component
extends="CFIDE.websocket.ChannelListener"
output="true"
hint="I define the application settings and event handlers."
{
// Store an instance of the Application.cfc that we'll use to
// process these WebSocket requests. This component
// (WSApplication), gets cached. As such, we'll have to re-
// instantiate the target Application component at key points
// during the lifecycle.
this.application = {};
// Log the listener instantiation.
logData( "WebSocket Channel Listener instantiated #now()#" );
// I teardown a subscription, removing any necessary settings
// for the given subscriber.
function afterUnsubscribe( requestInfo ){
// Re-instantiate the target application.
this.application = new Application();
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSRequestStart" )){
// Nothing to do.
return;
}
// Pass this off to the application for processing. Since
// this has no bearing on the request, we don't have to
// capture the response. This is purely a utilitarian call.
this.application.onWSRequestStart(
"unsubscribe",
requestInfo.channelName,
this.normalizeConnection( requestInfo.connectionInfo )
);
// Return out.
return;
}
// I determine if the given user can publish the given information.
function allowPublish( requestInfo ){
// Re-instantiate the target application.
this.application = new Application();
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSRequestStart" )){
// Nothing to do.
return( true );
}
// Pass this off to the application for processing.
var result = this.application.onWSRequestStart(
"publish",
requestInfo.channelName,
this.normalizeConnection( requestInfo.connectionInfo )
);
// Check to see if the request should be processed.
if (
isNull( result ) ||
!isBoolean( result ) ||
result
){
return( true );
}
// If we made it this far, the request should not processed.
return( false );
}
// I determine if the given user can subscribe to the given channel.
function allowSubscribe( requestInfo ){
// Re-instantiate the target application.
this.application = new Application();
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSRequestStart" )){
// Nothing to do.
return( true );
}
// Pass this off to the application for processing.
var result = this.application.onWSRequestStart(
"subscribe",
requestInfo.channelName,
this.normalizeConnection( requestInfo.connectionInfo )
);
// Check to see if the request should be processed.
if (
isNull( result ) ||
!isBoolean( result ) ||
result
){
return( true );
}
// If we made it this far, the request should not processed.
return( false );
}
// I initialize the message publication, allowing an opportunity
// to format and manipulate the message.
function beforePublish( message, requestInfo ){
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSRequest" )){
// Nothing to do.
return( message );
}
// Pass this off to the application for processing.
var result = this.application.onWSRequest(
requestInfo.channelName,
this.normalizeConnection( requestInfo.connectionInfo ),
message
);
// Return the new message.
return( result );
}
// I initialize the message sending, allowing an opportunity to
// format and manipulate a message before it is sent to the
// given user.
function beforeSendMessage( message, requestInfo ){
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSResponse" )){
// Nothing to do.
return( message );
}
// Pass this off to the application for processing.
var result = this.application.onWSResponse(
requestInfo.channelName,
this.normalizeConnection( requestInfo.connectionInfo ),
message
);
// Return the new message.
return( result );
}
// I determine if the given message should be sent to the given
// client. This is invoked for EVERY client that is subscribed to
// to the given channel.
function canSendMessage( message, subscriberInfo, publisherInfo ){
// Check to see if the application will process this event.
if (!structKeyExists( this.application, "onWSResponseStart" )){
// Nothing to do.
return( true );
}
// Pass this off to the application for processing.
var result = this.application.onWSResponseStart(
subscriberInfo.channelName,
this.normalizeConnection( subscriberInfo.connectionInfo ),
this.normalizeConnection( publisherInfo.connectionInfo ),
message
);
// Check to see if the response should be processed.
if (
isNull( result ) ||
!isBoolean( result ) ||
result
){
return( true );
}
// If we made it this far, the response should not processed.
return( false );
}
// I normalize the connection infor making sure that is has the
// following fields:
//
// - authenticated
// - clientID
// - connectionTime
//
// If a channel has no subscribers or a message is published from
// the server, this information will be missing. To make
// processing easier, we're just gonna fill it in with defaults.
function normalizeConnection( connection ){
// Check to see if this connection is missing information.
if (isNull( connection.clientid )){
// Normalize. We're using quoted values to mimic the JSON
// keys that would have come across the connection.
connection[ "authenticated" ] = "NO";
connection[ "clientid" ] = 0;
connection[ "connectiontime" ] = now();
}
// Return the normalized connection.
return( connection );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// I log the arguments to the text file for debugging.
function logData( data ){
// Create a log file path for debugging.
var logFilePath = (
getDirectoryFromPath( getCurrentTemplatePath() ) &
"log.txt"
);
// Dump to TXT file.
writeDump( var=data, output=logFilePath );
}
}
// NOTE: CFScript tags added for Gist color-coding. Remove.
</cfscript>
As you can see, the WSApplication.cfc instantiates the Application.cfc component at the top of a number of the WebSocket event handlers (ex. allowPublish()). It's important to understand that the WSApplication.cfc component manually instantiates the Application.cfc component because this will affect the meaning of our log file output.
Ok, that said, let's take a look at the log.txt log file after a ColdFusion 10 WebSocket request (NOTE: I modified the output for display wrapping):
APPLICATION instantiated {ts '2012-03-12 10:01:53'}
*******************************************APPLICATION:
*******************************************
struct
applicationname: F6D39E6793A3C6F9C8D48840A1F2D6D1
*******************************************SESSION is NULL
*******************************************CGI:
*******************************************
struct
AUTH_PASSWORD: [empty string]
AUTH_TYPE: [empty string]
AUTH_USER: [empty string]
CERT_COOKIE: [empty string]
CERT_FLAGS: [empty string]
CERT_ISSUER: [empty string]
CERT_KEYSIZE: [empty string]
CERT_SECRETKEYSIZE: [empty string]
CERT_SERIALNUMBER: [empty string]
CERT_SERVER_ISSUER: [empty string]
CERT_SERVER_SUBJECT: [empty string]
CERT_SUBJECT: [empty string]
CF_TEMPLATE_PATH: /Applications/ColdFusion10/cfusion/wwwroot
-------> /Sites/bennadel.com/testing/coldfusion10/websockets3
-------> /WSApplication.cfc
CONTENT_LENGTH: [empty string]
CONTENT_TYPE: [empty string]
CONTEXT_PATH: /Sites/bennadel.com/testing/coldfusion10
-------> /websockets3/WSApplication.cfc
GATEWAY_INTERFACE: [empty string]
HTTPS: off
HTTPS_KEYSIZE: [empty string]
HTTPS_SECRETKEYSIZE: [empty string]
HTTPS_SERVER_ISSUER: [empty string]
HTTPS_SERVER_SUBJECT: [empty string]
HTTP_ACCEPT: [empty string]
HTTP_ACCEPT_ENCODING: [empty string]
HTTP_ACCEPT_LANGUAGE: [empty string]
HTTP_CONNECTION: [empty string]
HTTP_COOKIE: [empty string]
HTTP_HOST: [empty string]
HTTP_REFERER: [empty string]
HTTP_USER_AGENT: [empty string]
PATH_INFO: [empty string]
PATH_TRANSLATED: /Applications/ColdFusion10/cfusion/wwwroot
-------> /Sites/bennadel.com/testing/coldfusion10
-------> /websockets3/WSApplication.cfc
QUERY_STRING: [empty string]
REMOTE_ADDR: [empty string]
REMOTE_HOST: [empty string]
REMOTE_USER: [empty string]
REQUEST_METHOD: [empty string]
SCRIPT_NAME: /Sites/bennadel.com/testing/coldfusion10
-------> /websockets3/WSApplication.cfc/Sites/bennadel.com
-------> /testing/coldfusion10/websockets3/WSApplication.cfc
SERVER_NAME: none
SERVER_PORT: -1
SERVER_PORT_SECURE: 0
SERVER_PROTOCOL: none
SERVER_SOFTWARE: [empty string]
WEB_SERVER_API: [empty string]
*******************************************URL:
*******************************************
struct [empty]
*******************************************FORM:
*******************************************
struct [empty]
*******************************************REQUEST:
*******************************************
struct
cfdumpinited: FALSE
*******************************************COOKIE:
*******************************************
struct [empty]
*******************************************HTTP Request Data:
*******************************************
getHttpRequestData() Failed
*******************************************
The first thing you might notice in this output is that the WebSocket event listener instantiation was not logged. This is because ColdFusion caches an instance of the channel listener. It is only instantiated on the first request; all subsequent WebSocket requests are routed through the cached instance.
The second thing that you might notice is that the Application.cfc instantiation was only logged once. Since our WebSocket channel listener (WSApplication.cfc) manually instantiates the Application.cfc component, we can deduce that the Application.cfc component is not implicitly instantiated during a WebSocket request.
ColdFusion 10 WebSocket are not routed through the ColdFusion application framework. However, it is probably safe to assume that if your application has timed-out, a subsequent WebSocket request will cause an instantiation of the Application.cfc and the initialization of your ColdFusion application. I make this assumption because I also assume that the cached channel listener is stored in the same application memory space.
The next most important thing to notice in this log file is that the SESSION scope does not exist. If you look at the Application.cfc settings, you'll notice that I do have sessionManagement enabled; however, during a ColdFusion 10 WebSocket request, neither the session cookies nor the session scope are available.
WebSocket requests take place outside of the normal ColdFusion state management. If you want to know more information about the user making the WebSocket request, you'll have to look at the current connection info. The current connection info can be augmented within the onWSAuthenticate() event handler. It can also be augmented by the client, using custom request data headers.
I only just started looking into WebSockets in ColdFusion 10, so I'll hopefully have more on WebSocket-based state management later.
The CGI scope is also kind of strange. But that makes sense since this isn't a standard HTTP request. The one CGI property that seems the strangest to me is the SCRIPT_NAME. This appears to be duplicated twice within the same value. Odd.
The ColdFusion 10 WebSockets are pretty awesome! The idea of being able to Push data from a ColdFusion application is exhilarating. But, we have to understand that WebSockets require a special kind of request and a special kind of thinking. This isn't the standard page-request lifecycle and it isn't the standard ColdFusion application framework control flow. Without sessions, WebSocket requests operate more like REST calls. But, with custom connection data, we can still maintain cross-request user information.
Want to use code from this post? Check out the license.
Reader Comments
Ben,
I have a question about websockets and calling CFC files in other locations. In my particular setup, we have an SVN project that is basically a services folder that includes several CFC files. I can access any one of those files in the normal convention ("Services.folder.component"). I have a virtual directory in my IIS entry that allows for that Services folder to be called as such.
However, I've created a folder called "sockets" so that I should be able to access it in this manner ("Services.sockets.component"). When I do this, I cannot access it. Any idea why?
Folder Structure:
C:\websites\services\components\*
C:\websites\backend\*
Application.cfc resides in backend, along with CFM files. The CFM files will work if I place the components in the same project and change the way they are called ("component").
I am using something like this:
mySocket.invoke("Services.sockets.TicketSocketControl","getLatestInfo");
Where Services.sockets.TicketSocketControl is the CFC, and getLatestInfo is the method.
I guess to be thorough, I should report that the issue only occurs when trying to access the Alias using websockets, not with any other calls.
Ben,
I wanted to give you an update, so you wouldn't pull your hair out over this one. I found the problem. Working in javascript (I should have seen this right off the bat), the Java pathing will not directly work. You have to pass in the relative path or an absolute path, which is kind of scary in either sense. For example:
"services.sockets.component" was resolving to:
C:\Coldfusion10\cfusion\bin\services\sockets\component.cfc
The way I found to access it was:
"/websites/services/sockets/component" which resolves to:
C:\websites\services\sockets\component.cfc
The problem I have with that, is that your file path is exposed directly off of the C:\. Any idea on a workaround?