Index | Thread | Search

From:
Rafael Sadowski <rafael@sizeofvoid.org>
Subject:
add custom HTTP header support to httpd
To:
tech@openbsd.org
Date:
Fri, 26 Dec 2025 12:01:22 +0100

Download raw body.

Thread
Add custom HTTP header support to httpd

Allow httpd.conf to add custom HTTP response headers or suppress
existing ones. This enables httpd to add security headers, custom
metadata, or remove unwanted headers without modifying app code.

Three new directives are added:

    header add <name> <value>
        Add custom header to successful responses (2xx, 3xx)

    header always add <name> <value>
        Add custom header to all responses including errors

    header hide <name>
        Suppress a response header

Headers can be specified in server blocks and are inherited by
location blocks unless the location defines its own headers.

Example configuration:

    server "example.com" {
        listen on * tls port 443

        header always add "X-Frame-Options" "SAMEORIGIN"
        header always add "X-Content-Type-Options" "nosniff"
        header add "X-Powered-By" "OpenBSD httpd"

        location "/api/*" {
            header add "Access-Control-Allow-Origin" "*"
        }
    }

The implementation follows the existing pattern form FastCGI parameters.
So I didn't have to invent anything new. Much of the code is from
existing patterns (so the potential bugs too). Just to give one example:
The entire IMSG part.

The motivation was to have something similar to ngx_http_headers_module
without relayd coming into play. For this reason I follow the nginx pattern:

"Adds the specified field to a response header provided that the
response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304,
307 (1.1.16, 1.0.13), or 308 (1.13.0). Parameter value can contain
variables."

-- https://nginx.org/en/docs/http/ngx_http_headers_module.html

Tests, feedback... welcome.

Rafael

diff --git a/usr.sbin/httpd/config.c b/usr.sbin/httpd/config.c
index b45081129b7..b115a1a8efa 100644
--- a/usr.sbin/httpd/config.c
+++ b/usr.sbin/httpd/config.c
@@ -196,6 +196,7 @@ clear_config_server_ptrs(struct server_config *cfg)
 
 	/* clear TAILQ_HEAD */
 	memset(&cfg->fcgiparams, 0, sizeof(cfg->fcgiparams));
+	memset(&cfg->headers, 0, sizeof(cfg->headers));
 
 	/* clear TAILQ_ENTRY */
 	memset(&cfg->entry, 0, sizeof(cfg->entry));
@@ -281,6 +282,9 @@ config_setserver(struct httpd *env, struct server *srv)
 
 			/* Configure FCGI parameters if necessary. */
 			config_setserver_fcgiparams(env, srv);
+
+			/* Configure custom headers if necessary. */
+			config_setserver_headers(env, srv);
 		}
 	}
 
@@ -442,6 +446,104 @@ config_setserver_fcgiparams(struct httpd *env, struct server *srv)
 	return (0);
 }
 
+int
+config_getserver_headers(struct httpd *env, struct imsg *imsg)
+{
+	struct server		*srv;
+	struct server_config	*srv_conf, *iconf;
+	struct custom_header	*hdr;
+	uint32_t		 id;
+	size_t			 c, nc, len;
+	uint8_t			*p = imsg->data;
+
+	len = sizeof(nc) + sizeof(id);
+	if (IMSG_DATA_SIZE(imsg) < len) {
+		log_debug("%s: invalid message length", __func__);
+		return (-1);
+	}
+
+	memcpy(&nc, p, sizeof(nc));	/* number of headers */
+	p += sizeof(nc);
+
+	memcpy(&id, p, sizeof(id));	/* server conf id */
+	srv_conf = serverconfig_byid(id);
+	p += sizeof(id);
+
+	len += nc*sizeof(*hdr);
+	if (IMSG_DATA_SIZE(imsg) < len) {
+		log_debug("%s: invalid message length", __func__);
+		return (-1);
+	}
+
+	/* Find associated server config */
+	TAILQ_FOREACH(srv, env->sc_servers, srv_entry) {
+		if (srv->srv_conf.id == id) {
+			srv_conf = &srv->srv_conf;
+			break;
+		}
+		TAILQ_FOREACH(iconf, &srv->srv_hosts, entry) {
+			if (iconf->id == id) {
+				srv_conf = iconf;
+				break;
+			}
+		}
+	}
+
+	/* Fetch custom headers */
+	for (c = 0; c < nc; c++) {
+		if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
+			fatalx("headers out of memory");
+		memcpy(hdr, p, sizeof(*hdr));
+		TAILQ_INSERT_HEAD(&srv_conf->headers, hdr, entry);
+
+		p += sizeof(*hdr);
+	}
+
+	return (0);
+}
+
+int
+config_setserver_headers(struct httpd *env, struct server *srv)
+{
+	struct privsep		*ps = env->sc_ps;
+	struct server_config	*srv_conf = &srv->srv_conf;
+	struct custom_header	*hdr;
+	struct iovec		*iov;
+	size_t			 c = 0, nc = 0;
+
+	DPRINTF("%s: sending headers for \"%s[%u]\" to %s fd %d", __func__,
+	    srv_conf->name, srv_conf->id, ps->ps_title[PROC_SERVER],
+	    srv->srv_s);
+
+	if (TAILQ_EMPTY(&srv_conf->headers))	/* nothing to do */
+		return (0);
+
+	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
+		nc++;
+	}
+	if ((iov = calloc(nc + 2, sizeof(*iov))) == NULL)
+		return (-1);
+
+	iov[c].iov_base = &nc;			/* number of headers */
+	iov[c++].iov_len = sizeof(nc);
+	iov[c].iov_base = &srv_conf->id;	/* server config id */
+	iov[c++].iov_len = sizeof(srv_conf->id);
+
+	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {	/* push headers */
+		iov[c].iov_base = hdr;
+		iov[c++].iov_len = sizeof(*hdr);
+	}
+	if (proc_composev(ps, PROC_SERVER, IMSG_CFG_HEADERS, iov, c) != 0) {
+		log_warn("%s: failed to compose IMSG_CFG_HEADERS imsg for "
+		    "`%s'", __func__, srv_conf->name);
+		free(iov);
+		return (-1);
+	}
+	free(iov);
+
+	return (0);
+}
+
 int
 config_setserver_tls(struct httpd *env, struct server *srv)
 {
@@ -640,6 +742,20 @@ config_getserver_config(struct httpd *env, struct server *srv,
 
 		srv_conf->flags |= parent->flags & SRVFLAG_NO_BANNER;
 
+		/* Inherit custom headers from parent if location has none */
+		if (TAILQ_EMPTY(&srv_conf->headers)) {
+			struct custom_header *hdr, *hdr_copy;
+			TAILQ_FOREACH(hdr, &parent->headers, entry) {
+				if ((hdr_copy = calloc(1, sizeof(*hdr_copy))) == NULL)
+					goto fail;
+				/* Copy only data fields, not TAILQ_ENTRY */
+				strlcpy(hdr_copy->name, hdr->name, sizeof(hdr_copy->name));
+				strlcpy(hdr_copy->value, hdr->value, sizeof(hdr_copy->value));
+				hdr_copy->flags = hdr->flags;
+				TAILQ_INSERT_TAIL(&srv_conf->headers, hdr_copy, entry);
+			}
+		}
+
 		DPRINTF("%s: %s %d location \"%s\", "
 		    "parent \"%s[%u]\", flags: %s",
 		    __func__, ps->ps_title[privsep_process], ps->ps_instance,
diff --git a/usr.sbin/httpd/httpd.conf.5 b/usr.sbin/httpd/httpd.conf.5
index b3673284e0b..1b3cbea2504 100644
--- a/usr.sbin/httpd/httpd.conf.5
+++ b/usr.sbin/httpd/httpd.conf.5
@@ -464,6 +464,32 @@ Enable static gzip compression to save bandwidth.
 If gzip encoding is accepted and if the requested file exists with
 an additional .gz suffix, use the compressed file instead and deliver
 it with content encoding gzip.
+.It Ic header Ar option
+Manipulate HTTP response headers.
+Multiple header statements may be specified.
+Valid options are:
+.Bl -tag -width Ds
+.It Ic add Ar name Ar value
+Add a custom HTTP response header with the specified
+.Ar name
+and
+.Ar value .
+The header will be added to successful responses (2xx and 3xx status codes).
+.It Ic always add Ar name Ar value
+Add a custom HTTP response header that is included in all responses,
+including error responses (4xx and 5xx status codes).
+.It Ic hide Ar name
+Suppress an HTTP response header with the specified
+.Ar name .
+This can be used to remove headers that are added by default,
+such as
+.Qq Last-Modified
+or custom headers inherited from a parent server configuration.
+.El
+.Pp
+Header names are limited to 127 characters and values to 511 characters.
+Headers specified in a location block inherit headers from the parent
+server configuration unless the location defines its own headers.
 .It Ic hsts Oo Ar option Oc
 Enable HTTP Strict Transport Security.
 Valid options are:
@@ -895,6 +921,29 @@ server "example.com" {
 	}
 }
 .Ed
+.Pp
+Custom HTTP headers can be added to responses for security or compatibility:
+.Bd -literal -offset indent
+server "example.com" {
+	listen on * tls port 443
+
+	# Add security headers to all responses
+	header always add "X-Frame-Options" "SAMEORIGIN"
+	header always add "X-Content-Type-Options" "nosniff"
+	header always add "Content-Security-Policy" "default-src 'self'"
+
+	# Add custom header to successful responses only
+	header add "X-Powered-By" "OpenBSD httpd"
+
+	# Hide default Last-Modified header
+	header hide "Last-Modified"
+
+	location "/api/*" {
+		# Location inherits parent headers and can add more
+		header add "Access-Control-Allow-Origin" "*"
+	}
+}
+.Ed
 .Sh SEE ALSO
 .Xr htpasswd 1 ,
 .Xr glob 7 ,
diff --git a/usr.sbin/httpd/httpd.h b/usr.sbin/httpd/httpd.h
index baf11d1b523..a6dd2fb8a22 100644
--- a/usr.sbin/httpd/httpd.h
+++ b/usr.sbin/httpd/httpd.h
@@ -209,6 +209,7 @@ enum imsg_type {
 	IMSG_CFG_MEDIA,
 	IMSG_CFG_AUTH,
 	IMSG_CFG_FCGI,
+	IMSG_CFG_HEADERS,
 	IMSG_CFG_DONE,
 	IMSG_LOG_ACCESS,
 	IMSG_LOG_ERROR,
@@ -470,6 +471,17 @@ struct fastcgi_param {
 };
 TAILQ_HEAD(server_fcgiparams, fastcgi_param);
 
+struct custom_header {
+	char			name[128];
+	char			value[512];
+	uint8_t			flags;
+#define HEADER_HIDE		0x01
+#define HEADER_ALWAYS		0x02
+
+	TAILQ_ENTRY(custom_header) entry;
+};
+TAILQ_HEAD(server_headers, custom_header);
+
 struct server_config {
 	uint32_t		 id;
 	uint32_t		 parent_id;
@@ -540,6 +552,7 @@ struct server_config {
 
 	struct server_fcgiparams fcgiparams;
 	int			 fcgistrip;
+	struct server_headers	 headers;
 	char			 errdocroot[HTTPD_ERRDOCROOT_MAX];
 
 	TAILQ_ENTRY(server_config) entry;
@@ -673,6 +686,8 @@ void	 server_http(void);
 int	 server_httpdesc_init(struct client *);
 void	 server_read_http(struct bufferevent *, void *);
 void	 server_abort_http(struct client *, unsigned int, const char *);
+void	 server_add_custom_headers(struct server_config *, struct kvtree *,
+	    int, unsigned int);
 unsigned int
 	 server_httpmethod_byname(const char *);
 const char
@@ -825,9 +840,11 @@ int	 config_getcfg(struct httpd *, struct imsg *);
 int	 config_setserver(struct httpd *, struct server *);
 int	 config_setserver_tls(struct httpd *, struct server *);
 int	 config_setserver_fcgiparams(struct httpd *, struct server *);
+int	 config_setserver_headers(struct httpd *, struct server *);
 int	 config_getserver(struct httpd *, struct imsg *);
 int	 config_getserver_tls(struct httpd *, struct imsg *);
 int	 config_getserver_fcgiparams(struct httpd *, struct imsg *);
+int	 config_getserver_headers(struct httpd *, struct imsg *);
 int	 config_setmedia(struct httpd *, struct media_type *);
 int	 config_getmedia(struct httpd *, struct imsg *);
 int	 config_setauth(struct httpd *, struct auth *);
diff --git a/usr.sbin/httpd/parse.y b/usr.sbin/httpd/parse.y
index 326b249662f..f5fbd02e524 100644
--- a/usr.sbin/httpd/parse.y
+++ b/usr.sbin/httpd/parse.y
@@ -142,6 +142,7 @@ typedef struct {
 %token	ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS REWRITE
 %token	CA CLIENT CRL OPTIONAL PARAM FORWARDED FOUND NOT
 %token	ERRDOCS GZIPSTATIC BANNER
+%token	HEADER ADD ALWAYS HIDE
 %token	<v.string>	STRING
 %token  <v.number>	NUMBER
 %type	<v.port>	port
@@ -323,6 +324,7 @@ server		: SERVER optmatch STRING	{
 			SPLAY_INIT(&srv->srv_clients);
 			TAILQ_INIT(&srv->srv_hosts);
 			TAILQ_INIT(&srv_conf->fcgiparams);
+			TAILQ_INIT(&srv_conf->headers);
 
 			TAILQ_INSERT_TAIL(&srv->srv_hosts, srv_conf, entry);
 		} '{' optnl serveropts_l '}'	{
@@ -557,6 +559,7 @@ serveroptsl	: LISTEN ON STRING opttls port	{
 		| root
 		| directory
 		| banner
+		| header
 		| logformat
 		| fastcgi
 		| authenticate
@@ -643,6 +646,8 @@ serveroptsl	: LISTEN ON STRING opttls port	{
 			srv = s;
 			srv_conf = &srv->srv_conf;
 			SPLAY_INIT(&srv->srv_clients);
+			TAILQ_INIT(&srv_conf->fcgiparams);
+			TAILQ_INIT(&srv_conf->headers);
 		} '{' optnl serveropts_l '}'	{
 			struct server	*s = NULL;
 			uint32_t	 f;
@@ -710,6 +715,82 @@ banner		: BANNER		{
 		}
 		;
 
+header		: HEADER HIDE STRING	{
+			struct custom_header	*hdr;
+
+			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
+				fatal("out of memory");
+
+			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
+			    sizeof(hdr->name)) {
+				yyerror("header name truncated");
+				free($3);
+				free(hdr);
+				YYERROR;
+			}
+			free($3);
+
+			hdr->flags = HEADER_HIDE;
+			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
+		}
+		| HEADER ADD STRING STRING	{
+			struct custom_header	*hdr;
+
+			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
+				fatal("out of memory");
+
+			if (strlcpy(hdr->name, $3, sizeof(hdr->name)) >=
+			    sizeof(hdr->name)) {
+				yyerror("header name truncated");
+				free($3);
+				free($4);
+				free(hdr);
+				YYERROR;
+			}
+			free($3);
+
+			if (strlcpy(hdr->value, $4, sizeof(hdr->value)) >=
+			    sizeof(hdr->value)) {
+				yyerror("header value truncated");
+				free($4);
+				free(hdr);
+				YYERROR;
+			}
+			free($4);
+
+			hdr->flags = 0;
+			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
+		}
+		| HEADER ALWAYS ADD STRING STRING	{
+			struct custom_header	*hdr;
+
+			if ((hdr = calloc(1, sizeof(*hdr))) == NULL)
+				fatal("out of memory");
+
+			if (strlcpy(hdr->name, $4, sizeof(hdr->name)) >=
+			    sizeof(hdr->name)) {
+				yyerror("header name truncated");
+				free($4);
+				free($5);
+				free(hdr);
+				YYERROR;
+			}
+			free($4);
+
+			if (strlcpy(hdr->value, $5, sizeof(hdr->value)) >=
+			    sizeof(hdr->value)) {
+				yyerror("header value truncated");
+				free($5);
+				free(hdr);
+				YYERROR;
+			}
+			free($5);
+
+			hdr->flags = HEADER_ALWAYS;
+			TAILQ_INSERT_TAIL(&srv->srv_conf.headers, hdr, entry);
+		}
+		;
+
 optfound	: /* empty */	{ $$ = 0; }
 		| FOUND		{ $$ = 1; }
 		| NOT FOUND	{ $$ = -1; }
@@ -1447,7 +1528,9 @@ lookup(char *s)
 	/* this has to be sorted always */
 	static const struct keywords keywords[] = {
 		{ "access",		ACCESS },
+		{ "add",		ADD },
 		{ "alias",		ALIAS },
+		{ "always",		ALWAYS },
 		{ "authenticate",	AUTHENTICATE},
 		{ "auto",		AUTO },
 		{ "backlog",		BACKLOG },
@@ -1475,6 +1558,8 @@ lookup(char *s)
 		{ "forwarded",		FORWARDED },
 		{ "found",		FOUND },
 		{ "gzip-static",	GZIPSTATIC },
+		{ "header",		HEADER },
+		{ "hide",		HIDE },
 		{ "hsts",		HSTS },
 		{ "include",		INCLUDE },
 		{ "index",		INDEX },
diff --git a/usr.sbin/httpd/server.c b/usr.sbin/httpd/server.c
index 5d5063b6480..e63ae942035 100644
--- a/usr.sbin/httpd/server.c
+++ b/usr.sbin/httpd/server.c
@@ -503,6 +503,7 @@ serverconfig_reset(struct server_config *srv_conf)
 	srv_conf->tls_ocsp_staple = NULL;
 	srv_conf->tls_ocsp_staple_file = NULL;
 	TAILQ_INIT(&srv_conf->fcgiparams);
+	TAILQ_INIT(&srv_conf->headers);
 }
 
 struct server *
@@ -1369,6 +1370,9 @@ server_dispatch_parent(int fd, struct privsep_proc *p, struct imsg *imsg)
 	case IMSG_CFG_FCGI:
 		config_getserver_fcgiparams(httpd_env, imsg);
 		break;
+	case IMSG_CFG_HEADERS:
+		config_getserver_headers(httpd_env, imsg);
+		break;
 	case IMSG_CFG_DONE:
 		config_getcfg(httpd_env, imsg);
 		break;
diff --git a/usr.sbin/httpd/server_fcgi.c b/usr.sbin/httpd/server_fcgi.c
index f3c01a459b0..b0e57194d39 100644
--- a/usr.sbin/httpd/server_fcgi.c
+++ b/usr.sbin/httpd/server_fcgi.c
@@ -730,6 +730,8 @@ server_fcgi_header(struct client *clt, unsigned int code)
 	    kv_add(&resp->http_headers, "Date", tmbuf) == NULL))
 		return (-1);
 
+	server_add_custom_headers(srv_conf, &resp->http_headers, 0, code);
+
 	if (server_writeresponse_http(clt) == -1 ||
 	    server_bufferevent_print(clt, "\r\n") == -1 ||
 	    server_headers(clt, resp, server_writeheader_http, NULL) == -1 ||
diff --git a/usr.sbin/httpd/server_http.c b/usr.sbin/httpd/server_http.c
index 485b67bc5e8..18a9fe2e1b3 100644
--- a/usr.sbin/httpd/server_http.c
+++ b/usr.sbin/httpd/server_http.c
@@ -54,6 +54,7 @@ char		*server_expand_http(struct client *, const char *,
 		    char *, size_t);
 char		*replace_var(char *, const char *, const char *);
 char		*read_errdoc(const char *, const char *);
+char		*get_always_custom_headers(struct server_config *);
 
 static struct http_method	 http_methods[] = HTTP_METHODS;
 static struct http_error	 http_errors[] = HTTP_ERRORS;
@@ -891,6 +892,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	char			 tmbuf[32], hbuf[128], *hstsheader = NULL;
 	char			*clenheader = NULL;
 	char			*bannerheader = NULL, *bannertoken = NULL;
+	char			*customheaders = NULL;
 	char			 buf[IBUF_READ_SIZE];
 	char			*escapedmsg = NULL;
 	char			 cstr[5];
@@ -1059,6 +1061,8 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 			goto done;
 		}
 
+	customheaders = get_always_custom_headers(srv_conf);
+
 	/* Add basic HTTP headers */
 	if (asprintf(&httpmsg,
 	    "HTTP/1.0 %03d %s\r\n"
@@ -1069,6 +1073,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	    "%s"
 	    "%s"
 	    "%s"
+	    "%s"
 	    "\r\n"
 	    "%s",
 	    code, httperr, tmbuf,
@@ -1076,6 +1081,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	    clenheader == NULL ? "" : clenheader,
 	    extraheader == NULL ? "" : extraheader,
 	    hstsheader == NULL ? "" : hstsheader,
+	    customheaders == NULL ? "" : customheaders,
 	    desc->http_method == HTTP_METHOD_HEAD || clenheader == NULL ?
 	    "" : body) == -1)
 		goto done;
@@ -1091,6 +1097,7 @@ server_abort_http(struct client *clt, unsigned int code, const char *msg)
 	free(clenheader);
 	free(bannerheader);
 	free(bannertoken);
+	free(customheaders);
 	if (msg == NULL)
 		msg = "\"\"";
 	if (asprintf(&httpmsg, "%s (%03d %s)", msg, code, httperr) == -1) {
@@ -1560,6 +1567,76 @@ server_locationaccesstest(struct server_config *srv_conf, const char *path)
 	    (ret == 0 && SRVFLAG_LOCATION_NOT_FOUND & srv_conf->flags));
 }
 
+/*
+ * Add or remove custom headers configured for this server.
+ * Headers marked as always are included in all responses.
+ * Regular headers are only added to 2xx and 3xx responses.
+ */
+void
+server_add_custom_headers(struct server_config *srv_conf,
+    struct kvtree *headers, int is_error, unsigned int code)
+{
+	struct custom_header	*hdr;
+	struct kv		*kv, search;
+
+	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
+		if (is_error && !(hdr->flags & HEADER_ALWAYS))
+			continue;
+
+		if (!is_error && !(hdr->flags & HEADER_ALWAYS)) {
+			if (code != 200 && code != 201 && code != 204 &&
+			    code != 206 && code != 301 && code != 302 &&
+			    code != 303 && code != 304 && code != 307 &&
+			    code != 308)
+				continue;
+		}
+
+		if (hdr->flags & HEADER_HIDE) {
+			/* Remove header from response */
+			search.kv_key = hdr->name;
+			if ((kv = kv_find(headers, &search)) != NULL)
+				kv_delete(headers, kv);
+		} else {
+			/* Add header to response */
+			kv_add(headers, hdr->name, hdr->value);
+		}
+	}
+}
+
+/*
+ * Build a raw custom HTTP header.
+ * only includes headers marked as always.
+ * Returns the string or NULL on error/no headers.
+ */
+char *
+get_always_custom_headers(struct server_config *srv_conf)
+{
+	struct custom_header *hdr;
+	char *headers = NULL;
+	char *tmp = NULL;
+
+	TAILQ_FOREACH(hdr, &srv_conf->headers, entry) {
+		if (!(hdr->flags & HEADER_ALWAYS) || hdr->flags & HEADER_HIDE)
+			continue;
+
+		if (headers == NULL) {
+			if (asprintf(&headers, "%s: %s\r\n", hdr->name,
+			    hdr->value) == -1) {
+				return (NULL);
+			}
+		} else {
+			if (asprintf(&tmp, "%s%s: %s\r\n", headers, hdr->name,
+			    hdr->value) == -1) {
+				free(headers);
+				return (NULL);
+			}
+			free(headers);
+			headers = tmp;
+		}
+	}
+	return (headers);
+}
+
 int
 server_response_http(struct client *clt, unsigned int code,
     struct media_type *media, off_t size, time_t mtime)
@@ -1627,6 +1704,8 @@ server_response_http(struct client *clt, unsigned int code,
 			return (-1);
 	}
 
+	server_add_custom_headers(srv_conf, &resp->http_headers, 0, code);
+
 	/* Date header is mandatory and should be added as late as possible */
 	if (server_http_time(time(NULL), tmbuf, sizeof(tmbuf)) <= 0 ||
 	    kv_add(&resp->http_headers, "Date", tmbuf) == NULL)