Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Julie Dion and Catherine Neault
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Julie Dion Catherine Neault

Handling 401 Unauthorized Responses In Turbo Drive And ColdFusion

By
Published in ,

For the past few months, I've been digging into the use of the Hotwire framework in a ColdFusion context. And, for the most part, everything I've looked at so far has represented the "happy path" - all that which happens when everything goes right. Now, I want to start looking at some failure cases - the so-called "sad path." In today's post, I'm considering ways to respond to an unauthorized request in a ColdFusion application.

View this code in my ColdFusion + Hotwire Demos project on GitHub.

Handling errors in a Hotwire application is more complicated than a vanilla ColdFusion application because requests can be made in different contexts. Some requests are made as top-level page requests; and, can seamlessly follow 301 / 302 responses. Other requests are made in a Turbo Frame context; and, don't provide any built-in mechanism for breaking out of the frame on error.

Thankfully, Turbo Drive includes a special HTTP header when it is making requests in a Turbo Frame context: Turbo-Frame. If this HTTP header is present (the id of the contextual frame), we can respond with a frame-specific error; and, if this HTTP header is absent, we can respond with a traditional location() based redirect.

To demonstrate this bifurcation, I've created a hard-coded oops.cfm file to represent our 401 Unauthorized request. If we're not in a Turbo Frame when this is called, I'll redirect to index.cfm - our login page. And, if we are in a Turbo Frame, I'm going to respond with a meaningful message to the user.

<cfscript>

	// If the UNAUTHORIZED request is being made OUTSIDE OF ANY TURBO FRAME, then we can
	// simply redirect the user back to the login page, the same way that we might for any
	// other ColdFusion application using an authentication / authorization wall.
	if ( ! request.turbo.isFrame ) {

		location( url = "./index.htm", addToken = false );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	// If the UNAUTHORIZED request is being made INSIDE A TURBO FRAME context, then
	// returning a redirect gets a bit tricky. The redirect will apply to the Turbo Frame
	// itself, not to the entire page. I'm not sure that there is a "right way" to do
	// this. For this demo, I'm going to return a static value (indicating the logged-out
	// state) with the option to also render a custom Turbo Stream element that performs
	// an automatic redirect.
	param name="url.useStream" type="boolean" default=false;

	header
		statusCode = 401
		statusText = "Unauthorized"
	;

</cfscript>
<cfoutput>
	<!--- Make sure to echo the correct frame ID. --->
	<turbo-frame id="#encodeForHtmlAttribute( request.turbo.frame )#">

		<p>
			You've been logged-out.
			<a href="./index.htm" data-turbo="false">Please login</a>
			to continue using the app.
		</p>

		<!---
			If the stream flag is enabled, this custom action will perform an automatic
			redirect of the top-level page.
		--->
		<cfif url.useStream>
			<turbo-stream
				action="visit"
				data-url="./index.htm">
			</turbo-stream>
		</cfif>

	</turbo-frame>
</cfoutput>

As you can see, if the unauthorized request is being made in the context of a Turbo Frame, I'm doing several things:

  • I'm making sure to echo the id of the Turbo Frame as provided by the Turbo-Frame HTTP header (extraction not shown in this code).

  • I'm providing a message about being logged-out, complete with an explicit link back to the login page.

  • I'm optionally including an inline Turbo Stream object to perform an automatic page-navigation to the login page. The visit action is a custom Turbo Stream action. You can think of this as invoking location() in a Turbo Frame context.

To try triggering this 401 Unauthorized request in both a top-level and frame-based context, I created another ColdFusion page - authorized.cfm - which allows for navigation actions to take place both at the top-level and at the frame-level. Both set of links are (essentially) the same; but, the latter set is scoped to a <turbo-frame> element. Both sets of links contain an oops.cfm target for the above error handling:

<cfscript>

	// For the sake of simplicity, all the "logged-in" pages will be rendered as this
	// page, using the "v" value to differentiate. 
	param name="url.v" type="string" default="home";

</cfscript>
<cfmodule template="./tags/page.cfm">
	<cfoutput>

		<h2>
			Page For #encodeForHtml( url.v.ucfirst() )#
		</h2>
		<p>
			<a href="authenticated.htm?v=home">Home</a> &mdash;
			<a href="authenticated.htm?v=activity">Activity</a> &mdash;
			<a href="authenticated.htm?v=profile">Profile</a> &mdash;
			<a href="oops.htm">Oops Page</a>
		</p>
		<p>
			This is the <strong>page content</strong> for
			<mark>[ #encodeForHtml( url.v )# ]</mark>.
		</p>

		<!---
			FRAME LEVEL page navigation options. These are all the same links; however,
			since they are defined inside a Turbo Frame, the are automatically scoped to
			the Turbo Frame instead of using a top-level navigation.
		--->
		<turbo-frame id="my-frame">
			<h3>
				Inside A Turbo Frame
			</h3>
			<p>
				<a href="authenticated.htm?v=home">Home</a> &mdash;
				<a href="authenticated.htm?v=activity">Activity</a> &mdash;
				<a href="authenticated.htm?v=profile">Profile</a> &mdash;
				<a href="oops.htm">Oops Page</a>
				( <a href="oops.htm?useStream=true">with Stream</a> )
			</p>
			<p>
				This is the <strong>frame content</strong> for
				<mark>[ #encodeForHtml( url.v )# ]</mark>.
			</p>
		</turbo-frame>

	</cfoutput>
</cfmodule>

Ok, let's try this out. First, we're going to log into the application and perform all of the navigation actions at the top-level:

Top-level navigation to an unauthorized page gets redirected to the login page.

As you can see, once we navigate to the oops.cfm page (our unauthorized request), we are automatically redirected to the Login page. This mirrors what we might do in a traditional ColdFusion application.

Now, let's try navigating in the context of the Turbo Frame:

Frame-level navigation to an unauthorized page gets a special rendering with a link back to the login page.

This time, once we hit the oops.cfm page, we can't just perform a location() call, otherwise it would be scoped to the Turbo Frame. Instead, we tell the user that they are logged-out and we provide them with a link back to the Login page.

As our last example, we're going to navigate in the context of the Turbo Frame; but, this time we'll ask for the inline Turbo Stream action to be included:

Frame-level navigation to an unauthorized page gets redirected to the login page with a custom Turbo Stream action.

This time, with the inline Turbo Stream action, we can see a behavior that is more akin to the first demo with the location() call. There is a flash of content since we're still rendering a response to the user. But, it's only there for a moment before the user is whisked away to the Login page.

Handling an unauthorized request at the top-level in a Hotwire application is pretty natural - it's the same thing we might do in a vanilla ColdFusion application. It's the Turbo-Frame-based request that adds complexity. But, I think this approach - with a message and an inline Turbo Stream action - feels like a nice solution.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel