Serving A Bypassable "Down For Maintenance" Page In ColdFusion 2021
In the vast majority of cases, updates to my ColdFusion blog can be made while the site is online. Sometimes, however, if those changes are not backwards compatible, or require too much cross-file coordination, there's no way that I can start making changes without causing errors in the user experience (UX). In such cases, I need to temporarily block access to the site using a "Down for Maintenance" page. But, I still need to access the site in order to monitor and test the changes. As such, this maintenance page needs to be conditionally bypassable. Luckily, all of this is really easy in ColdFusion.
There's No One-Size-Fits-All Maintenance Page
To be clear, there are many ways to implement a maintenance page in a web application. For example, at InVision, we manage our maintenance page at the outer-most ALB (Application Load Balancer). This way, we actually prevent traffic from entering most of our network.
You could also change the routing or virtual-host bindings at the web server level - think nginx, Apache, or Microsoft's Internet Information Service (IIS). This way, you're actually rendering a different "site" when users hit your domain.
In a ColdFusion application, you can override the page template in your onRequestStart()
event-handler. This allows you to leverage the full-force of your ColdFusion business logic when rendering the maintenance page. But, it means that your ColdFusion application has to be boot-strappable; which it may not.
Which leads me to the approach that I am using, as outlined in this blog post. As you'll see below, I'm including the maintenance page template as the very first operation in my Application.cfc
- a template which renders the maintenance page for the user and then aborts the rest of the request. This approach limits what we can do in the maintenance page (for example, there is no application
scope). But, it also means that we don't have to worry about our ColdFusion application being in a "working state".
Including the Maintenance Page Template
This simple approach works by using the CFInclude
tag to invoke the maintenance template as the very first operation inside my Application.cfc
ColdFusion application component. I do this even before I define the application name or any of the application settings:
component
output = false
hint = "I define the application settings and event handlers."
{
include "./maintenance.cfm";
// Define application settings.
this.name = "WwwBenNadelCom";
this.applicationTimeout = createTimeSpan( 2, 0, 0, 0 );
this.sessionManagement = false;
this.setClientCookies = false;
// .... truncated for demo ....
}
To be clear, I do not leave this CFInclude
in the code all the time. During normal website operation, this line would be commented-out. However, when I need to take the site offline in order to perform non-trivial updates, I'll uncomment the CFInclude
statement and then redeploy the Application.cfc
file.
This maintenance.cfm
template is a stand-alone ColdFusion template that performs several actions:
Sends the correct HTTP Headers and status code for an unavailable website.
Renders the maintenance page.
Offers a cached version of the requested page to the user (via Google).
Aborts the request so that the request does not try to bootstrap - or depend on - the ColdFusion application.
Conditionally allows me to access the underlying ColdFusion application based on a simple Cookie value. This way, I can test the updates to the site even while the maintenance page is live.
Here's my current version of this template:
<cfscript>
// Check cookies to see if the current request should bypass the maintenance page.
// --
// NOTE: We're not "securing" the active site - we simply hiding it from users while
// it is undergoing some work. As such, this check here doesn't have to be very
// effective - it just has to provide a nice user experience (UX).
if ( ! compare( cookie?.earlyAccess, "PleaseToBeHavingEarlyAccess!" ) ) {
exit;
}
// Google allows users to search for cached versions of pages. As a convenience, let's
// link to the search results for the cached version of the requested page.
searchUrl = "https://www.google.com/search?q=#encodeForUrl( "cache:https://www.bennadel.com#cgi.path_info#" )#";
</cfscript>
<!---
Report to the browser that this page is not representative of the active application.
And, that the client should check back in 30-min to see if the site is back online.
--->
<cfheader statusCode="503" statusText="Service Temporarily Unavailable" />
<cfheader name="Retry-After" value="#( 60 * 30 )#" />
<cfcontent type="text/html; charset=utf-8" />
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
Down for Maintenance - BenNadel.com
</title>
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" />
<style type="text/css">
body {
color: #333333 ;
font-family: "nunito sans", arial, helvetica, sans-serif ;
font-size: 1.2rem ;
font-weight: 400 ;
line-height: 1.5 ;
margin: 20px auto 20px auto ;
max-width: 650px ;
padding: 0px 20px 0px 20px ;
text-align: center ;
}
h1:before,
h1:after {
content: "\1F6A7" ; /* Construction sign emoji. */
}
p {
margin-bottom: 30px ;
}
p.cache {
font-weight: 700 ;
}
p.cache strong {
background-color: #ffe05a ;
display: inline-block ;
padding: 0px 4px 0px 4px ;
text-decoration: inherit ;
}
a {
color: inherit ;
}
strong {
white-space: nowrap ;
}
</style>
</head>
<body>
<h1>
Down for Maintenance
</h1>
<p>
<strong>So Sorry</strong> — I am currently applying updates to my site that
cannot be performed while the site is online. This should only take a few minutes.
Thank you for your patience. Please check back shortly.
</p>
<p class="cache">
<a href="<cfoutput>#searchUrl#</cfoutput>" target="blank">
Try viewing the <strong>cached version</strong> of this page on
<strong>Google</strong>
</a>
→
</p>
<p>
Maintenance window started at <strong>6:05 AM EST</strong>.
</p>
</body>
</html>
<!--- Don't let anything else in the request get processed. --->
<cfabort />
Notice that the very last thing in this CFML template is a CFAbort
tag. This terminates the processing of the request. And, since this template is being processed as an include in the Application.cfc
file, it means that it terminates the instantiation of the Application.cfc
component - the component that is instantiated on every request to a ColdFusion page. This prevents the request from bootstrapping the underlying ColdFusion application, which means that we don't have to worry about any breaking-changes in our ColdFusion logic.
At this point, if I made a request to my ColdFusion blog without doing anything special with the cookies, I would see this page:
As you can see, the user gets the "Down for Maintenance" page. And, since this is happening as the first statement in the Application.cfc
file, this is the page that will be rendered no matter which URL a user requests.
At this point, we can't depend on any logic in the underlying ColdFusion application - from this template's perspective, that application doesn't actually exist yet (hasn't been associated with the request). But, we can still depend on the native behavior of the ColdFusion application server. Which means that we can access the cgi
scope; which, in turn, gives us access to the request information. In this case, I'm using the cgi.path_info
to see what URL the user asked for. And then, I'm using that to information to provide a link to Google's cached version of the requested page.
Once I've confirmed that the maintenance page is indeed being rendered for the users, I then update my cookies to allow me to access the underlying ColdFusion application. At the top of the maintenance CFML template, I'm simply checking to see if a cookie-value exists; and, if so, I exit out of the maintenance template:
Here, I'm using the Edit This Cookie Chrome extension to manually add the maintenance page bypass cookie to my current browser session. And, once this cookie is in place, my next request to the site bypasses the maintenance template and allows the request to enter the underlying ColdFusion application:
At that point, I can personally test the updates being made to the site even while everyone else is still seeing the down for maintenance page.
Once I'm done validating all the changes, I then comment-out the CFInclude
statement at the top of the Application.cfc
ColdFusion application component and then redeploy it. At this point, all incoming traffic will be directed to the ColdFusion application and all will be happy!
Again, I must emphasize that there are a thousand-and-one ways to accomplish something like this. This is just the approach that I use because it balances functionality with simplicity. This approach may not work for your situation.
That Cookie Isn't Very Secure
You might look at this approach and think it's not very secure. After all, couldn't anyone just add that cookie to their browser and bypass the maintenance page? Well, sure. But to what end? This maintenance page isn't here to secure anything - it's here to provide a nice user experience while the underlying application is being updated. If a user were to prematurely bypass this maintenance page, they wouldn't see anything "secret" - they'd probably just get some sort of runtime error.
Don't Override The Error Response Content
It's been a while since I've had to deal with this problem; but, I'm pretty sure that some web servers (such as IIS) will override error responses by default. Meaning, if your ColdFusion application returns a non-200 status code, the web server will ignore your content and serve up some static error page. This is a setting you can override in the web server - you can tell it to serve-up whatever content your ColdFusion application server returned.
Want to use code from this post? Check out the license.
Reader Comments
Thanks for your post on a maintenance page with a bypass!
I've implemented this, but the one problem I've found with it is that as long as status code 503 is returned, the graceful "Down for Maintenance" page will display only when the website is called without a particular page specified. Otherwise, it displays the standard "Service Temporarily Unavailable" message but never loads and displays the maintenance page which explains things to users.
In other words, the designed maintenance page displays ONLY when I call the domain WITHOUT adding a file name to the URL. It won't display to anyone who is trying to get to a particular page, though, again, the 503 error notice displays always.
Do you have any insight into this? Thanks again.
@Jeff,
That's really interesting. If you could never get the website's version of the maintenance page to serve-up, then I would suggest that this is an issue with the web server overriding non-200 responses. But, since you can get it sometime, then clearly the web server is allowing non-200 responses to be served from the web application.
This makes me wonder if there's an error being thrown somewhere in the maintenance page template when no resource is being provided. So, when you have a deep URL into the site, some value is set and can be consumed in the template. But, when no deep URL is provided (ie, just the domain), then some variable reference is failing.
Perhaps you can try wrapping the entire template in a
try/catch
and then outputting the error. Also, you might want to try checking your error logs for anything suspicious on those requests.But, yeah, that's a strange one!
Good suggestions - thanks!
It also occurred to me to relocate this process to the header.cfm file we use with all our individual pages, rather than in application.cfc, just to see if the behavior is any different.
I'll let you know if I discover anything useful as I continue to work on it.
Jeff
@Jeff,
Good luck! That touch-point between the web server and the application server can get tricky. Let us know what you find.
@Ben Nadel,
I could not find any errors on the maintenance page, nor did the situation change when I moved the trigger process from application.cfc to our page header. In other words, if the 503 status code is sent, the process breaks down for any page beyond the unvarnished domain address (www.catholicculture.org). However two considerations:
Since we are on a Microsoft Windows server, It would not surprise me if this is related to how IIS handles the issue, and I haven't yet tried to look into that;
Our need for this sort of maintenance technique (that is, maintenance requring the website to be operational for an administrator but not for anyone else) occurs very seldom. The only instance may be when we need to change our encryption key to adhere to credit card processing requirements (PCI - Payment Card Industry requirements), which of course must be done when no users are accessing encrypted fields. For us, this is typically once every three years, for about an hour or so. Other uses would be similarly very rare. Therefore, I don't think we need to worry much about the consequences for search engines of not sending the 503 status code.
So I remain happy with the solution, and thank you again for teaching me how to do it! Still, if I do ever figure it out, I'll let you know.
Jeff
@Jeff,
Well, at least we tried! 😛 I'm almost certain this has something to do with how IIS is intercepting the errors. Or some other URL rewriting issue. But yeah, if this isn't something that comes up very often, no need to go way down the rabbit hole. Good luck!
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →