Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Joel Hill ( @Jiggidyuo )

Hotwire Turbo Drive Requires Failed Form Submissions To Return A non-2xx Status Code

By on

Over the past few weeks, I've been exploring the use of Hotwire in a ColdFusion application. It's a fascinating framework (from Basecamp) that forces you to think about web fundamentals and how to progressively enhance the user experience (UX). This morning, I ran into an issue trying to get Turbo Drive to work with HTTP Form submissions. It turns out, Turbo Drive requires non-2xx status codes to be returned in response to a failed form submission in ColdFusion.

The way Turbo Drive works is by intercepting links and form submissions, turning those "page" requests in "AJAX" requests, and then merging the response payload into the current page. With form submissions, this gets a little tricky since the browser has native functionality for resubmitting a form that Hotwire cannot reproduce:

The reason Turbo doesn't allow regular rendering on 200's from POST requests is that browsers have built-in behavior for dealing with reloads on POST visits where they present a "Are you sure you want to submit this form again?" dialogue that Turbo can't replicate. Instead, Turbo will stay on the current URL upon a form submission that tries to render, rather than change it to the form action, since a reload would then issue a GET against that action URL, which may not even exist.

When I first wired-up the forms in my ColdFusion and Hotwire proof-of-concept, my form processing code looked like this - note the catch block logic that does nothing by define the errorMessage:

<cfscript>

	// Setup defaults for form fields.
	param name="request.context.name" type="string" default="";
	param name="request.context.occupation" type="string" default="";
	param name="request.context.notes" type="string" default="";
	param name="request.context.submitted" type="boolean" default=false;

	request.template.title = "Add a new Tippee";

	errorMessage = "";

	if ( request.context.submitted ) {

		try {

			result = application.tippeeWorkflow.createTippee(
				name = request.context.name,
				occupation = request.context.occupation,
				notes = request.context.notes
			);

			// On success, redirect to the DETAIL PAGE of the newly-created entity.
			location(
				url = "/index.htm?event=tippee.view&id=#encodeForUrl( result.id )#&showAddSuccess=true",
				addToken = false
			);

		} catch ( any error ) {

			application.logService.logException( error );

			// On error, set the appropriate error message for the given error - this is
			// what will be shown to the user.
			errorMessage = application.errorService.getResponseMessage( error );

		}

	}

</cfscript>

As you can see, when the form submission data is invalid, and an error is thrown by my Workflow / Use-Case component (tippeeWorkflow), my processing logic catches the error and uses it to set the error message. The browser, however, doesn't know that this special control-flow took place and all it sees is a 200 OK response with some rendered HTML.

As such, when I run this ColdFusion application alongside Turbo Drive, submitting invalid form data leads to the following console error:

Turbo Drive error: Form responses must redirect to another location.

My 200 OK response to a failed form submission results in the error:

Error: Form responses must redirect to another location

And, the rendered HTML is completely unchanged, making it look as if nothing happened at all when the user submitted the form.

To be clear, I was in the wrong here. When the server cannot process the request, it should return a non-2xx response. To fix this, I went back into my processing code and made sure to set both the response code and the error message:

<cfscript>

	// Setup defaults for form fields.
	param name="request.context.name" type="string" default="";
	param name="request.context.occupation" type="string" default="";
	param name="request.context.notes" type="string" default="";
	param name="request.context.submitted" type="boolean" default=false;

	request.template.title = "Add a new Tippee";

	errorMessage = "";

	if ( request.context.submitted ) {

		try {

			result = application.tippeeWorkflow.createTippee(
				name = request.context.name,
				occupation = request.context.occupation,
				notes = request.context.notes
			);

			// On success, redirect to the DETAIL PAGE of the newly-created entity.
			location(
				url = "/index.htm?event=tippee.view&id=#encodeForUrl( result.id )#&showAddSuccess=true",
				addToken = false
			);

		} catch ( any error ) {

			application.logService.logException( error );

			// NOTE: In order for Turbo Drive to work with properly with FORM SUBMISSIONS,
			// we must set an appropriate HTTP status code on the response. Turbo Drive
			// expects a "Location" (redirect) header when a form is submitted
			// successfully. As such, in order for our form to re-render on a failed
			// submission, we must return a non-200 status code.
			errorResponse = application.errorService.getResponse( error );
			request.template.statusCode = errorResponse.statusCode;
			request.template.statusText = errorResponse.statusText;

			// On error, set the appropriate error message for the given error - this is
			// what will be shown to the user.
			errorMessage = errorResponse.message;

		}

	}

</cfscript>

This time, instead of getting the error message directly from the error, we're getting the error response. The response contains the status code information as well as the message to display to the user. And, this time, when we submit an invalid form request, we get the following response:

Turbo Drive successfully merged the 422 Unprocessable Entity HTML response into the current view.

As you can see, now that I'm returning a 422 Unprocessable Entity HTTP status code, Turbo Drive knows how to handle the response and successfully merges the returned HTML into the current browser rendering, showing the new error message to the user.

This is what I mean when I said that Hotwire forces to think about "web fundamentals". If this were just a ColdFusion application, returning a 200 OK with a failed form submission would have worked fine. It would have been "incorrect", semantically; but, the user wouldn't have noticed. With Turbo Drive in place, however, I'm forced to think more deeply about how I'm handling my control flow.

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

Reader Comments

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

Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.
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