Serving error pages from HAProxy
A.k.a. a terrible, terrible hack. There are hints around this solution on the internets, but I couldn't find a good explanation of what's going on, so here you go.
How?
example.cfg
listen example
mode http
bind 127.0.0.1:8000
errorfile 503 /tmp/example.http
/tmp/example.http
HTTP/1.0 400 Bad request
Cache-Control: no-cache
Connection: close
Content-Type: text/html
<html><body><h1>400 Bad request</h1>
This is an example.
</body></html>
Running
haproxy -f example.cfg
What? Why?
Alright, let's try this a different way.
example.cfg
frontend example-frn
mode http
bind 127.0.0.1:8000
use_backend example-bck
backend example-bck
mode http
errorfile 503 /tmp/example.http
The other file is the same.
Seriously, what?
Here's the relevant part from the documentation.
errorfile <code> <file> returns a file contents instead of errors generated by HAProxy. [...] The files are returned verbatim on the TCP socket. This allows any trick such as redirections to another URL or site, as well as tricks to clean cookies, force enable or disable caching, etc... The package provides default error files returning the same contents as default errors.
Translation: we can tell HAProxy to, when serving a specific error code, instead of serving its built-in error content, serve the contents of a file. BUT! That file is "returned verbatim on the TCP socket", including HTTP headers and all. This allows all kinds of tricks, and one trick that isn't mentioned is changing the HTTP response code.
The last ingredient: if a listen
or backend
directive doesn't have
any UP
servers, it will return a 503
.
Combining all these, we can say: hey HAProxy, here's a backend that
will never have a server because I won't configure any. (So you'll
always serve 503 on this backend). And oh, HAProxy, whenever you'd
serve a 503
, serve this file instead. (And the file just so happens
to contain a 400
response, but it could be whatever else we want).
Caveats
Again, from the documentation:
The files should not exceed the configured buffer size (BUFSIZE), which generally is 8 or 16 kB, otherwise they will be truncated. It is also wise not to put any reference to local contents (eg: images) in order to avoid loops between the client and HAProxy when all servers are down, causing an error to be returned instead of an image. For better HTTP compliance, it is recommended that all header lines end with CR-LF and not LF alone.
The files are read at the same time as the configuration and kept in memory. For this reason, the errors continue to be returned even when the process is chrooted, and no file change is considered while the process is running. A simple method for developing those files consists in associating them to the 403 status code and interrogating a blocked URL.
So basically, keep the file small and simple. In practice this means
that probably no user-facing error pages should be served by HAProxy;
you're better of delegating that to a web server. This technique can
still be useful for handling cases that users are not expected to
encounter. For example in the hosting of this blog, I'm using it to
restrict incoming requests to two domains, and return a descriptive
error if HAProxy receives a request with a different Host
header.
What's your favorite HAProxy trick?