Serve Dynamic Custom Error Pages With HAProxy

Set up custom error pages in HAProxy to ensure consistent, branded messaging that supports any backend web stack.

The memory is probably still fresh: You’re shopping online at your favorite website, looking for something specific, you’ve got it narrowed down to two or maybe three products, you make the final decision, click to checkout, and then— Internal Server Error. A cryptic error has replaced the page you were expecting. More than surprised, you feel knocked off balance. Where do you go from here?

When a customer comes across an error, there can be a number of causes: the page doesn’t exist, there was a network-related error, or today was the day that a bug buried deep inside the code decided to manifest itself. Whatever the cause, the customer is now in a state of dismay and it’s imperative that you guide them back to the main site and, if possible, restore their faith in your company. One way to do that is by showing a better error page.

There are plenty of examples of customized error pages that aim to delight and entertain users who are unfortunate enough to come across them. For inspiration, check this list by Designmodo and this one from Canva. By customizing your error pages, you can keep the same tone as the rest of your website, using on-brand colors, images, and voice. The big challenge is finding a proven way to serve custom error pages, one that works with any web server technology or a mix of technologies.

As we’ll cover here, you can guarantee the consistent and reliable delivery of custom error pages by storing them in your HAProxy load balancer, which sits in front of your web servers. HAProxy relays requests and responses between clients and servers, and if it detects an error, it will replace the bleak, unbranded error page with the one you’ve created. By delegating this task to your load balancer, you guarantee consistent delivery of these pages, even if the backend servers have crashed and are no longer reachable.

HAProxy version 2.2 expanded support for custom error pages by introducing dynamic error handling with the new http-errors section, which makes it easy to assign different error pages to different websites and to create error pages that return different data formats, such as JSON. It also added functionality to intercept and return a response without contacting backend servers at all, which is ideal for serving maintenance pages and other types of status pages.

Add Custom Error Pages to HAProxy

To add a custom error page, start by creating a new folder under /etc/haproxy such as /etc/haproxy/errors that will hold your error page files. Next, define your error pages. They should have the .http file extension since they include both HTML markup and the HTTP status code and response headers. Here is an example 404 error page file, which you could store at /etc/haproxy/errors/404.http:

HTTP/1.1 404 Not Found
Cache-Control: no-cache
Connection: close
Content-Type: text/html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>404 Not Found</title>
</head>
<body>
<main>
<h1>404 Not Found</h1>
This is my custom 404 Not Found page!
</main>
</body>
</html>

Next, open the HAProxy configuration file, /etc/haproxy/haproxy.cfg, and add a section called http-errors that contains an errorfile directive that points to the 404.http file. This goes at the same level as a frontend or backend section:

http-errors myerrors
errorfile 404 /etc/haproxy/errors/404.http

You can create more .http files to handle other statuses, such as 500 Server Error and 403 Forbidden, and then add lines to the http-errors section for them. HAProxy supports any of the following status codes: 200, 400, 401, 403, 404, 405, 407, 408, 410, 425, 429, 500, 502, 503, and 504.

http-errors myerrors
errorfile 400 /etc/haproxy/errors/400.http
errorfile 401 /etc/haproxy/errors/401.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 404 /etc/haproxy/errors/404.http

By default, HAProxy will serve these files only when it triggers the error itself. For example, if HAProxy can’t reach any of your backend servers it will trigger a 503 Service Unavailable error. Or, if it successfully reaches a server, but then exceeds its timeout while waiting for a reply, it will return a 504 Gateway Timeout error. You can configure it to return your custom error files for these types of errors by including an errorfiles directive in your frontend.

To replace other errors, such as 404 Not Found and 500 Server Error, you’ll need to check which status code the server returned and then have HAProxy replace the response using that same code. Consider this frontend section, which overwrites the standard 404 error page returned by the server:

http-errors myerrors
errorfile 404 /etc/haproxy/errors/404.http
frontend site1
bind :80
default_backend webservers
errorfiles myerrors
http-response return status 404 default-errorfiles if { status 404 }

The http-response return line intercepts responses that have a status of 404 and returns a custom error page from the myerrors section. Its default-errorfiles parameter tells it to use the files referenced by the errorfiles directive.

Here’s the result when accessing a page that doesn’t exist:

accessing a page that doesn’t exist

A custom 404 error page

Per-Site Custom Error Pages

Now that you’ve seen how to return custom error pages, we’ll take a look at other ways to fine-tune them. By placing an errorfiles directive into a frontend, as shown in the previous section, you are indicating which http-errors section to use. However, some people proxy more than one website through the same frontend and then route requests to different backends depending on the host header received. In that case, you could use conditional statements to select a different http-errors section dynamically depending on the website the user is accessing.

In the next example, one set of error pages is returned if the website is site1.com and a different one is returned for site2.com. This works by checking the incoming host header, which shows the name of the website, and then executing the http-response return line that matches that name by using an if statement.

http-errors site1
errorfile 404 /etc/haproxy/errors/site1-404.http
http-errors site2
errorfile 404 /etc/haproxy/errors/site2-404.http
frontend allsites
bind :80
default_backend site1-servers
use_backend site2-servers if { req.hdr(host) site2.com }
# Store host header in variable
http-request set-var(txn.host) req.hdr(host)
# Use site1 error page if site1.com
http-response return status 404 errorfiles site1 if { status 404 } { var(txn.host) -m str site1.com }
# Use site2 error page if site2.com
http-response return status 404 errorfiles site2 if { status 404 } { var(txn.host) -m str site2.com }

Note that you need to store the host header in a variable by using the http-request set-var directive, since HAProxy won’t have access to it during the response phase otherwise. HAProxy is capable of proxying many websites through the same frontend and then choosing the appropriate backend based on conditional logic. With this technique, you’re able to match error pages with the requested site.

Rendering Dynamic Content

Going beyond returning a static HTML page, you may also find it helpful to include extra details that the customer could screenshot and send back to you. For example, you could record a unique ID in your HAProxy logs for each request and then display that ID on the error page so that it’s easier to find the corresponding log line later. You can also show other information, such as the HTTP request headers, the client’s IP address, cookie values, and so forth.

Consider this frontend section:

frontend site1
bind :80
default_backend webservers
errorfiles myerrors
unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
unique-id-header X-Unique-ID
log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r %[unique-id]"
http-response return status 404 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/404.html if { status 404 }

The unique-id-format directive creates a unique identifier for each request using the given format. In this example, the unique ID consists of the client’s IP address and port, the frontend’s IP address and port, the state indicating how the request was terminated, the request counter, and the HAProxy process ID, all encoded as hexadecimal.

A request’s unique ID is forwarded to the backend server as an HTTP header by including the unique-id-header directive with the name you want to give to the header, such as X-Unique-ID. It’s also included in the logs by appending the unique-id fetch method to the end of a custom log format, which is set with log-format. Last, we show it to the customer on the error page by including it in the 404.html file. Note that we’re using the lf-file parameter on the http-response return line so that the HTML file becomes a template that can render HAProxy variables and fetch methods. Here are the contents of the 404 HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>404 Not Found</title>
</head>
<body>
<main>
<h1>404 Not Found</h1>
<p>This is my custom 404 Not Found page!</p>
<p>Unique ID: %[unique-id]</p>
</main>
</body>
</html>

Here is the result:

404 not found page with unique id

A 404 error page with a unique ID

While using http-response return with the lf-file parameter works well for replacing server errors with templated HTML files, it won’t capture errors generated from HAProxy itself. Typically, you would use errorfiles to specify static error pages to use for those. However, in version 2.2, you can use the http-error status directive to set template files for HAProxy-generated errors too, using its lf-file parameter. Its syntax is nearly identical to http-response return, except that it does not allow an if statement to follow it.

Return JSON Errors

For services that normally return JSON-formatted responses, you’ll want to create custom error pages that return JSON instead of HTML. For instance, you could add the following file as /etc/haproxy/errors/503-json.http:

HTTP/1.1 503 Service Unavailable
Cache-Control: no-cache
Connection: close
Content-Type: application/json
{ "errors" : [ { "status" : "503", "title" : "Service unavailable", "detail" : "No server is available to handle this request." } ] }

Then add a new http-errors section to your HAProxy configuration and reference it in your service’s frontend section. You can use the errorfiles directive to intercept only errors that HAProxy emits or use http-response return to override other errors from the servers:

http-errors json
errorfile 404 /etc/haproxy/errors/404-json.http
errorfile 503 /etc/haproxy/errors/503-json.http
frontend api
bind :8080
default_backend apiservers
errorfiles json
http-response return status 404 default-errorfiles if { status 404 }

The JSON-formatted 503 Service Unavailable response will be returned for this service when all backend servers are down.

an error page rendered as jnson

An error page rendered as JSON

Maintenance Pages

HAProxy version 2.2 added another helpful feature: the ability to return responses without contacting the backend server. The new native response generator introduces the http-request return directive, which returns content directly from HAProxy. There’s quite a bit you can do with this, even building up small services such as the one Daniel Corbett created here, for which the configuration is here, which hashes a string using various hashing algorithms. Its hashing functionality comes from HAProxy’s built-in converters. The page you return can display properties that HAProxy captures, giving you access to HAProxy’s fetch methods and converters.

Let’s use http-request return to create a simple maintenance page that tells visitors that the site is currently undergoing some scheduled work. First, add the file /etc/haproxy/maintenance.html with the following markup:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Site Undergoing Maintenance</title>
</head>
<body>
The site is undergoing scheduled maintenance.
</body>
</html>

Then, update your haproxy.cfg file to return this file by using the http-request return directive:

frontend site1
bind :80
default_backend webservers
http-request return status 200 content-type text/html file /etc/haproxy/errors/maintenance.html

Now, your website will display the maintenance page. Of course, you’ll want to style it better using your own company branding.

custom maintenance page

A custom maintenance page

Why You Need a Custom Error Page...

A well-crafted error page can restore a person’s faith in your brand. We only covered basic examples, but your own error pages can include CSS and Javascript that bring the page to life and help lighten the mood after receiving an error. By configuring HAProxy to handle returning these pages, you ensure consistency, even when all of your backend servers are offline. You can also render errors as JSON, categorize error pages by site, and return a maintenance page. Also, because HAProxy sits in front of your servers, this technique works with any backend web server technology.

Want to stay up to date on similar topics? Subscribe to this blog! You can also follow us on Twitter and join the conversation on Slack.

Interested in advanced security and administrative features? HAProxy Enterprise is the world’s fastest and most widely used software load balancer. It powers modern application delivery at any scale and in any environment, providing the utmost performance, observability, and security. Organizations harness its cutting edge features and enterprise suite of add-ons, backed by authoritative expert support and professional services. Ready to learn more? Sign up for a free trial.

Subscribe to our blog. Get the latest release updates, tutorials, and deep-dives from HAProxy experts.