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:
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:
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.
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.
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.