about summary refs log tree commit diff
diff options
context:
space:
mode:
authorLaurent Bercot <ska-skaware@skarnet.org>2023-10-18 13:07:23 +0000
committerLaurent Bercot <ska@appnovation.com>2023-10-18 13:07:23 +0000
commit1a7e3d00588725da3d8764fa9d624bc8611be070 (patch)
tree65d28b52bd1d4ccdbeb418d4d681b1999ed46b7a
parent24fb88dbb023ae08adc9989bf19e0c6c569a6607 (diff)
downloadtipidee-1a7e3d00588725da3d8764fa9d624bc8611be070.tar.gz
tipidee-1a7e3d00588725da3d8764fa9d624bc8611be070.tar.xz
tipidee-1a7e3d00588725da3d8764fa9d624bc8611be070.zip
Add infrastructure for custom error files
Signed-off-by: Laurent Bercot <ska@appnovation.com>
-rw-r--r--doc/tipidee.conf.html7
-rw-r--r--doc/tipideed.html10
-rw-r--r--package/deps.mak16
-rw-r--r--src/include/tipidee/conf.h1
-rw-r--r--src/include/tipidee/log.h2
-rw-r--r--src/include/tipidee/response.h10
-rw-r--r--src/libtipidee/deps-lib/tipidee4
-rw-r--r--src/libtipidee/tipidee_conf_get_errorfile.c24
-rw-r--r--src/libtipidee/tipidee_log_request.c2
-rw-r--r--src/libtipidee/tipidee_log_resource.c13
-rw-r--r--src/libtipidee/tipidee_response_error.c41
-rw-r--r--src/libtipidee/tipidee_response_error_nofile.c35
-rw-r--r--src/libtipidee/tipidee_response_file.c31
-rw-r--r--src/libtipidee/tipidee_response_header_common_put.c17
-rw-r--r--src/libtipidee/tipidee_response_status.c4
-rw-r--r--src/libtipidee/tipidee_rql_read.c5
-rw-r--r--src/tipideed/cgi.c144
-rw-r--r--src/tipideed/regular.c25
-rw-r--r--src/tipideed/responses.c80
-rw-r--r--src/tipideed/tipideed-internal.h73
-rw-r--r--src/tipideed/tipideed.c106
21 files changed, 379 insertions, 271 deletions
diff --git a/doc/tipidee.conf.html b/doc/tipidee.conf.html
index 76ffae1..483d9d0 100644
--- a/doc/tipidee.conf.html
+++ b/doc/tipidee.conf.html
@@ -407,9 +407,12 @@ malicious clients messing with log lines.
 This keyword has no effect when given without the <tt>request</tt> keyword. </dd>
  <dt> <tt>resource</tt> </dt> <dd> Log a <tt>resource</tt> line when <a href="tipideed.html">tipideed</a>
 has found a resource corresponding to the URI and is willing to serve it. The line looks like
-<tt>resource docroot <em>docroot</em> file <em>file</em> type <em>type</em></tt>. <em>docroot</em> is
-the document root of the virtual host; <em>file</em> is the path to the served file; <em>type</em>
+<tt>resource <em>file</em> type <em>type</em></tt>.
+<em>file</em> is the path to the served file; the first component of that path is always
+the document root of the virtual host. <em>type</em>
 is <tt>nph</tt> for an NPH script, <tt>cgi</tt> for a CGI script, or the Content-Type for a regular file.
+If it's a CGI or NPH script being called with a non-empty PATH_INFO, an additional <tt>path_info <em>path_info</em></tt>
+field is added to the log line.
 If a served CGI script outputs a local redirection, several <tt>resource</tt> lines may appear for
 a single request. </dd>
  <dt> <tt>answer</tt> </dt> <dd> Log an <tt>answer</tt> line when <a href="tipideed.html">tipideed</a>
diff --git a/doc/tipideed.html b/doc/tipideed.html
index 254fa2f..eebdb6d 100644
--- a/doc/tipideed.html
+++ b/doc/tipideed.html
@@ -120,8 +120,8 @@ underlying system. </dd>
 <p>
  tipideed expects the following variables in its environment, and will exit
 with an error message if they are undefined. When tipideed is run under
-<a href="//skarnet.org/s6-networking/s6-tcpserver.html">s6-tcpserver</a> or
-<a href="//skarnet.org/s6-networking/s6-tlsserver.html">s6-tlsserver</a>,
+<a href="//skarnet.org/software/s6-networking/s6-tcpserver.html">s6-tcpserver</a> or
+<a href="//skarnet.org/software/s6-networking/s6-tlsserver.html">s6-tlsserver</a>,
 these variables are automatically set by the super-server. This is the way
 tipidee gets its network information without having to perform network
 operations itself.
@@ -153,11 +153,11 @@ to CGI scripts. </dd>
 <p>
  tipideed can function without these variables, but if they're present, it
 uses them to get more information. They're typically obtained by calling
-<a href="//skarnet.org/s6-networking/s6-tcpserver-access.html">s6-tcpserver-access</a>
+<a href="//skarnet.org/software/s6-networking/s6-tcpserver-access.html">s6-tcpserver-access</a>
 before tipideed in the
-<a href="//skarnet.org/s6-networking/s6-tcpserver.html">s6-tcpserver</a>
+<a href="//skarnet.org/software/s6-networking/s6-tcpserver.html">s6-tcpserver</a>
 command line.
-(For HTTPS, <a href="//skarnet.org/s6-networking/s6-tlsserver.html">s6-tlsserver</a>
+(For HTTPS, <a href="//skarnet.org/software/s6-networking/s6-tlsserver.html">s6-tlsserver</a>
 calls it implicitly.)
 </p>
 
diff --git a/package/deps.mak b/package/deps.mak
index f389e28..3b29af0 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -18,6 +18,7 @@ src/libtipidee/tipidee_conf_free.o src/libtipidee/tipidee_conf_free.lo: src/libt
 src/libtipidee/tipidee_conf_get.o src/libtipidee/tipidee_conf_get.lo: src/libtipidee/tipidee_conf_get.c src/include/tipidee/conf.h
 src/libtipidee/tipidee_conf_get_argv.o src/libtipidee/tipidee_conf_get_argv.lo: src/libtipidee/tipidee_conf_get_argv.c src/include/tipidee/conf.h
 src/libtipidee/tipidee_conf_get_content_type.o src/libtipidee/tipidee_conf_get_content_type.lo: src/libtipidee/tipidee_conf_get_content_type.c src/include/tipidee/conf.h
+src/libtipidee/tipidee_conf_get_errorfile.o src/libtipidee/tipidee_conf_get_errorfile.lo: src/libtipidee/tipidee_conf_get_errorfile.c src/include/tipidee/conf.h
 src/libtipidee/tipidee_conf_get_redirection.o src/libtipidee/tipidee_conf_get_redirection.lo: src/libtipidee/tipidee_conf_get_redirection.c src/include/tipidee/conf.h
 src/libtipidee/tipidee_conf_get_string.o src/libtipidee/tipidee_conf_get_string.lo: src/libtipidee/tipidee_conf_get_string.c src/include/tipidee/conf.h
 src/libtipidee/tipidee_conf_get_uint32.o src/libtipidee/tipidee_conf_get_uint32.lo: src/libtipidee/tipidee_conf_get_uint32.c src/include/tipidee/conf.h
@@ -32,9 +33,10 @@ src/libtipidee/tipidee_log_request.o src/libtipidee/tipidee_log_request.lo: src/
 src/libtipidee/tipidee_log_resource.o src/libtipidee/tipidee_log_resource.lo: src/libtipidee/tipidee_log_resource.c src/include/tipidee/log.h
 src/libtipidee/tipidee_log_start.o src/libtipidee/tipidee_log_start.lo: src/libtipidee/tipidee_log_start.c src/include/tipidee/log.h
 src/libtipidee/tipidee_method.o src/libtipidee/tipidee_method.lo: src/libtipidee/tipidee_method.c src/include/tipidee/method.h
-src/libtipidee/tipidee_response_error.o src/libtipidee/tipidee_response_error.lo: src/libtipidee/tipidee_response_error.c src/include/tipidee/method.h src/include/tipidee/response.h src/include/tipidee/rql.h
+src/libtipidee/tipidee_response_error_nofile.o src/libtipidee/tipidee_response_error_nofile.lo: src/libtipidee/tipidee_response_error_nofile.c src/include/tipidee/method.h src/include/tipidee/response.h
+src/libtipidee/tipidee_response_file.o src/libtipidee/tipidee_response_file.lo: src/libtipidee/tipidee_response_file.c src/include/tipidee/conf.h src/include/tipidee/method.h src/include/tipidee/response.h src/include/tipidee/rql.h src/include/tipidee/util.h
 src/libtipidee/tipidee_response_header_builtin.o src/libtipidee/tipidee_response_header_builtin.lo: src/libtipidee/tipidee_response_header_builtin.c src/include/tipidee/config.h src/include/tipidee/response.h
-src/libtipidee/tipidee_response_header_common_put.o src/libtipidee/tipidee_response_header_common_put.lo: src/libtipidee/tipidee_response_header_common_put.c src/include/tipidee/config.h src/include/tipidee/response.h
+src/libtipidee/tipidee_response_header_common_put.o src/libtipidee/tipidee_response_header_common_put.lo: src/libtipidee/tipidee_response_header_common_put.c src/include/tipidee/response.h
 src/libtipidee/tipidee_response_header_date.o src/libtipidee/tipidee_response_header_date.lo: src/libtipidee/tipidee_response_header_date.c src/include/tipidee/response.h
 src/libtipidee/tipidee_response_header_date_fmt.o src/libtipidee/tipidee_response_header_date_fmt.lo: src/libtipidee/tipidee_response_header_date_fmt.c src/include/tipidee/response.h
 src/libtipidee/tipidee_response_header_lastmodified.o src/libtipidee/tipidee_response_header_lastmodified.lo: src/libtipidee/tipidee_response_header_lastmodified.c src/include/tipidee/response.h
@@ -44,11 +46,11 @@ src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_uri_parse.lo: src/libt
 src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_chunked_read.lo: src/libtipidee/tipidee_util_chunked_read.c src/include/tipidee/util.h
 src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_defaulttext.lo: src/libtipidee/tipidee_util_defaulttext.c src/include/tipidee/util.h
 src/libtipidee/tipidee_util_httpdate.o src/libtipidee/tipidee_util_httpdate.lo: src/libtipidee/tipidee_util_httpdate.c src/include/tipidee/util.h
-src/tipideed/cgi.o src/tipideed/cgi.lo: src/tipideed/cgi.c src/include/tipidee/headers.h src/include/tipidee/method.h src/include/tipidee/response.h src/include/tipidee/uri.h src/tipideed/tipideed-internal.h
+src/tipideed/cgi.o src/tipideed/cgi.lo: src/tipideed/cgi.c src/include/tipidee/tipidee.h src/tipideed/tipideed-internal.h
 src/tipideed/harden.o src/tipideed/harden.lo: src/tipideed/harden.c src/tipideed/tipideed-internal.h
 src/tipideed/options.o src/tipideed/options.lo: src/tipideed/options.c src/include/tipidee/log.h src/include/tipidee/response.h src/tipideed/tipideed-internal.h
 src/tipideed/regular.o src/tipideed/regular.lo: src/tipideed/regular.c src/include/tipidee/log.h src/include/tipidee/method.h src/include/tipidee/response.h src/tipideed/tipideed-internal.h
-src/tipideed/responses.o src/tipideed/responses.lo: src/tipideed/responses.c src/include/tipidee/log.h src/include/tipidee/response.h src/include/tipidee/rql.h src/tipideed/tipideed-internal.h
+src/tipideed/responses.o src/tipideed/responses.lo: src/tipideed/responses.c src/include/tipidee/log.h src/include/tipidee/response.h src/include/tipidee/util.h src/tipideed/tipideed-internal.h
 src/tipideed/send_file.o src/tipideed/send_file.lo: src/tipideed/send_file.c src/tipideed/tipideed-internal.h
 src/tipideed/tipideed.o src/tipideed/tipideed.lo: src/tipideed/tipideed.c src/include/tipidee/tipidee.h src/tipideed/tipideed-internal.h
 src/tipideed/trace.o src/tipideed/trace.lo: src/tipideed/trace.c src/include/tipidee/log.h src/include/tipidee/method.h src/include/tipidee/response.h src/tipideed/tipideed-internal.h
@@ -58,12 +60,12 @@ tipidee-config: src/config/tipidee-config.o src/config/confnode.o src/config/con
 tipidee-config-preprocess: EXTRA_LIBS := -lskarnet
 tipidee-config-preprocess: src/config/tipidee-config-preprocess.o
 ifeq ($(strip $(STATIC_LIBS_ARE_PIC)),)
-libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.o src/libtipidee/tipidee_conf_get.o src/libtipidee/tipidee_conf_get_argv.o src/libtipidee/tipidee_conf_get_content_type.o src/libtipidee/tipidee_conf_get_redirection.o src/libtipidee/tipidee_conf_get_string.o src/libtipidee/tipidee_conf_get_uint32.o src/libtipidee/tipidee_conf_init.o src/libtipidee/tipidee_headers_get_content_length.o src/libtipidee/tipidee_headers_init.o src/libtipidee/tipidee_headers_parse.o src/libtipidee/tipidee_headers_search.o src/libtipidee/tipidee_log_answer.o src/libtipidee/tipidee_log_exit.o src/libtipidee/tipidee_log_resource.o src/libtipidee/tipidee_log_request.o src/libtipidee/tipidee_log_start.o src/libtipidee/tipidee_method.o src/libtipidee/tipidee_response_error.o src/libtipidee/tipidee_response_header_builtin.o src/libtipidee/tipidee_response_header_common_put.o src/libtipidee/tipidee_response_header_date.o src/libtipidee/tipidee_response_header_date_fmt.o src/libtipidee/tipidee_response_header_lastmodified.o src/libtipidee/tipidee_response_status.o src/libtipidee/tipidee_rql_read.o src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_httpdate.o
+libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.o src/libtipidee/tipidee_conf_get.o src/libtipidee/tipidee_conf_get_argv.o src/libtipidee/tipidee_conf_get_content_type.o src/libtipidee/tipidee_conf_get_errorfile.o src/libtipidee/tipidee_conf_get_redirection.o src/libtipidee/tipidee_conf_get_string.o src/libtipidee/tipidee_conf_get_uint32.o src/libtipidee/tipidee_conf_init.o src/libtipidee/tipidee_headers_get_content_length.o src/libtipidee/tipidee_headers_init.o src/libtipidee/tipidee_headers_parse.o src/libtipidee/tipidee_headers_search.o src/libtipidee/tipidee_log_answer.o src/libtipidee/tipidee_log_exit.o src/libtipidee/tipidee_log_resource.o src/libtipidee/tipidee_log_request.o src/libtipidee/tipidee_log_start.o src/libtipidee/tipidee_method.o src/libtipidee/tipidee_response_error_nofile.o src/libtipidee/tipidee_response_file.o src/libtipidee/tipidee_response_header_builtin.o src/libtipidee/tipidee_response_header_common_put.o src/libtipidee/tipidee_response_header_date.o src/libtipidee/tipidee_response_header_date_fmt.o src/libtipidee/tipidee_response_header_lastmodified.o src/libtipidee/tipidee_response_status.o src/libtipidee/tipidee_rql_read.o src/libtipidee/tipidee_uri_parse.o src/libtipidee/tipidee_util_chunked_read.o src/libtipidee/tipidee_util_defaulttext.o src/libtipidee/tipidee_util_httpdate.o
 else
-libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error.lo src/libtipidee/tipidee_response_header_builtin.lo src/libtipidee/tipidee_response_header_common_put.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo
+libtipidee.a.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_header_builtin.lo src/libtipidee/tipidee_response_header_common_put.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo
 endif
 libtipidee.so.xyzzy: EXTRA_LIBS := -lskarnet
-libtipidee.so.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error.lo src/libtipidee/tipidee_response_header_builtin.lo src/libtipidee/tipidee_response_header_common_put.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo
+libtipidee.so.xyzzy: src/libtipidee/tipidee_conf_free.lo src/libtipidee/tipidee_conf_get.lo src/libtipidee/tipidee_conf_get_argv.lo src/libtipidee/tipidee_conf_get_content_type.lo src/libtipidee/tipidee_conf_get_errorfile.lo src/libtipidee/tipidee_conf_get_redirection.lo src/libtipidee/tipidee_conf_get_string.lo src/libtipidee/tipidee_conf_get_uint32.lo src/libtipidee/tipidee_conf_init.lo src/libtipidee/tipidee_headers_get_content_length.lo src/libtipidee/tipidee_headers_init.lo src/libtipidee/tipidee_headers_parse.lo src/libtipidee/tipidee_headers_search.lo src/libtipidee/tipidee_log_answer.lo src/libtipidee/tipidee_log_exit.lo src/libtipidee/tipidee_log_resource.lo src/libtipidee/tipidee_log_request.lo src/libtipidee/tipidee_log_start.lo src/libtipidee/tipidee_method.lo src/libtipidee/tipidee_response_error_nofile.lo src/libtipidee/tipidee_response_file.lo src/libtipidee/tipidee_response_header_builtin.lo src/libtipidee/tipidee_response_header_common_put.lo src/libtipidee/tipidee_response_header_date.lo src/libtipidee/tipidee_response_header_date_fmt.lo src/libtipidee/tipidee_response_header_lastmodified.lo src/libtipidee/tipidee_response_status.lo src/libtipidee/tipidee_rql_read.lo src/libtipidee/tipidee_uri_parse.lo src/libtipidee/tipidee_util_chunked_read.lo src/libtipidee/tipidee_util_defaulttext.lo src/libtipidee/tipidee_util_httpdate.lo
 tipideed: EXTRA_LIBS := -lskarnet
 tipideed: src/tipideed/tipideed.o src/tipideed/cgi.o src/tipideed/harden.o src/tipideed/options.o src/tipideed/regular.o src/tipideed/responses.o src/tipideed/send_file.o src/tipideed/tipideed.o src/tipideed/trace.o libtipidee.a.xyzzy
 INTERNAL_LIBS :=
diff --git a/src/include/tipidee/conf.h b/src/include/tipidee/conf.h
index abc8313..d0b8fb0 100644
--- a/src/include/tipidee/conf.h
+++ b/src/include/tipidee/conf.h
@@ -40,5 +40,6 @@ extern unsigned int tipidee_conf_get_argv (tipidee_conf const *, char const *, c
 extern char const *tipidee_conf_get_docroot (tipidee_conf const *, tipidee_uri const *, uint16_t) ;
 extern int tipidee_conf_get_redirection (tipidee_conf const *, char const *, size_t, char const *, tipidee_redirection *) ;
 extern char const *tipidee_conf_get_content_type (tipidee_conf const *, char const *) ;
+extern char const *tipidee_conf_get_errorfile (tipidee_conf const *, char const *, unsigned int) ;
 
 #endif
diff --git a/src/include/tipidee/log.h b/src/include/tipidee/log.h
index 7453bb7..bf3b812 100644
--- a/src/include/tipidee/log.h
+++ b/src/include/tipidee/log.h
@@ -39,7 +39,7 @@ extern void tipidee_log_start (uint32_t, char const *, char const *) ;
 extern void tipidee_log_exit (uint32_t, unsigned int) ;
 
 extern void tipidee_log_request (uint32_t, tipidee_rql const *, tipidee_headers const *, stralloc *) ;
-extern void tipidee_log_resource (uint32_t, tipidee_rql const *, char const *, char const *, tipidee_resattr const *) ;
+extern void tipidee_log_resource (uint32_t, tipidee_rql const *, char const *, tipidee_resattr const *, char const *) ;
 extern void tipidee_log_answer (uint32_t, tipidee_rql const *, unsigned int, uint64_t) ;
 
 #define tipidee_log_debug(v, ...) do { if ((v) & TIPIDEE_LOG_DEBUG) strerr_warn(PROG, ": debug: ", __VA_ARGS__) ; } while (0)
diff --git a/src/include/tipidee/response.h b/src/include/tipidee/response.h
index d969d9c..3850970 100644
--- a/src/include/tipidee/response.h
+++ b/src/include/tipidee/response.h
@@ -3,11 +3,13 @@
 #ifndef TIPIDEE_RESPONSE_H
 #define TIPIDEE_RESPONSE_H
 
-#include <sys/stat.h>
+#include <skalibs/bsdsnowflake.h>
+
 #include <stddef.h>
 #include <stdint.h>
 
 #include <skalibs/gccattributes.h>
+#include <skalibs/stat.h>
 #include <skalibs/buffer.h>
 #include <skalibs/strerr.h>
 #include <skalibs/tai.h>
@@ -31,7 +33,11 @@ extern size_t tipidee_response_header_lastmodified (char *, size_t, struct stat
 extern size_t tipidee_response_header_common_put (buffer *, uint32_t, tain const *) ;
 #define tipidee_response_header_common_put_g(b, options) tipidee_response_header_common_put(b, (options), &STAMP)
 
-extern size_t tipidee_response_error (buffer *, tipidee_rql const *, unsigned int, char const *, char const *, uint32_t) ;
+size_t tipidee_response_file (buffer *, tipidee_rql const *, unsigned int, char const *, struct stat const *, char const *, uint32_t, tain const *) ;
+#define tipidee_response_file_g(b, rql, status, reason, st, ct, options) tipidee_response_file(b, rql, status, reason, st, ct, (options), &STAMP)
+
+extern size_t tipidee_response_error_nofile (buffer *, tipidee_rql const *, unsigned int, char const *, char const *, uint32_t, tain const *) ;
+#define tipidee_response_error_nofile_g(b, rql, status, reason, text, options) tipidee_response_error_nofile(b, rql, status, reason, text, (options), &STAMP)
 
 extern tipidee_response_header_builtin const *tipidee_response_header_builtin_table ;
 extern char const *tipidee_response_header_builtin_search (char const *) ;
diff --git a/src/libtipidee/deps-lib/tipidee b/src/libtipidee/deps-lib/tipidee
index cba7e43..93e261a 100644
--- a/src/libtipidee/deps-lib/tipidee
+++ b/src/libtipidee/deps-lib/tipidee
@@ -2,6 +2,7 @@ tipidee_conf_free.o
 tipidee_conf_get.o
 tipidee_conf_get_argv.o
 tipidee_conf_get_content_type.o
+tipidee_conf_get_errorfile.o
 tipidee_conf_get_redirection.o
 tipidee_conf_get_string.o
 tipidee_conf_get_uint32.o
@@ -16,7 +17,8 @@ tipidee_log_resource.o
 tipidee_log_request.o
 tipidee_log_start.o
 tipidee_method.o
-tipidee_response_error.o
+tipidee_response_error_nofile.o
+tipidee_response_file.o
 tipidee_response_header_builtin.o
 tipidee_response_header_common_put.o
 tipidee_response_header_date.o
diff --git a/src/libtipidee/tipidee_conf_get_errorfile.c b/src/libtipidee/tipidee_conf_get_errorfile.c
new file mode 100644
index 0000000..732ff9d
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_errorfile.c
@@ -0,0 +1,24 @@
+/* ISC license. */
+
+#include <string.h>
+#include <errno.h>
+
+#include <skalibs/types.h>
+
+#include <tipidee/conf.h>
+
+char const *tipidee_conf_get_errorfile (tipidee_conf const *conf, char const *docroot, unsigned int status)
+{
+  size_t docrootlen = strlen(docroot) ;
+  char key[7 + docrootlen] ;
+  if (status < 100 || status > 999) goto err ;
+  key[0] = 'E' ; key[1] = ':' ;
+  uint_fmt(key + 2, status) ;
+  key[5] = ':' ;
+  memcpy(key + 6, docroot, docrootlen + 1) ;
+  return tipidee_conf_get_string(conf, key) ;
+
+ err:
+  errno = EINVAL ;
+  return 0 ;
+}
diff --git a/src/libtipidee/tipidee_log_request.c b/src/libtipidee/tipidee_log_request.c
index a50169d..d633f0b 100644
--- a/src/libtipidee/tipidee_log_request.c
+++ b/src/libtipidee/tipidee_log_request.c
@@ -17,7 +17,7 @@ void tipidee_log_request (uint32_t v, tipidee_rql const *rql, tipidee_headers co
   size_t start = sa->len ;  /* assert: not zero */
   size_t refpos = 0, uapos = 0 ;
   if (!(v & TIPIDEE_LOG_REQUEST)) return ;
-  if (!string_quotes(sa, rql->uri.path)) goto eerr ;
+  if (!string_quotes(sa, rql->uri.path) || !stralloc_0(sa)) goto eerr ;
   if (v & TIPIDEE_LOG_REFERRER)
   {
     char const *x = tipidee_headers_search(hdr, "Referrer") ;
diff --git a/src/libtipidee/tipidee_log_resource.c b/src/libtipidee/tipidee_log_resource.c
index b730612..75196b0 100644
--- a/src/libtipidee/tipidee_log_resource.c
+++ b/src/libtipidee/tipidee_log_resource.c
@@ -6,9 +6,9 @@
 
 #include <tipidee/log.h>
 
-void tipidee_log_resource (uint32_t v, tipidee_rql const *rql, char const *docroot, char const *file, tipidee_resattr const *ra)
+void tipidee_log_resource (uint32_t v, tipidee_rql const *rql, char const *file, tipidee_resattr const *ra, char const *infopath)
 {
-  char const *a[8] = { PROG, ": info:" } ;
+  char const *a[10] = { PROG, ": info:" } ;
   size_t m = 2 ;
   if (!(v & TIPIDEE_LOG_RESOURCE)) return ;
   if (v & TIPIDEE_LOG_HOSTASPREFIX)
@@ -16,11 +16,14 @@ void tipidee_log_resource (uint32_t v, tipidee_rql const *rql, char const *docro
     a[m++] = " host " ;
     a[m++] = rql->uri.host ;
   }
-  a[m++] = " resource docroot " ;
-  a[m++] = docroot ;
-  a[m++] = " file " ;
+  a[m++] = " resource " ;
   a[m++] = file ;
   a[m++] = " type " ;
   a[m++] = ra->iscgi ? ra->isnph ? "nph" : "cgi" : ra->content_type ;
+  if (ra->iscgi && infopath)
+  {
+    a[m++] = " path_info /" ;
+    a[m++] = infopath ;
+  }
   strerr_warnv(a, m) ;
 }
diff --git a/src/libtipidee/tipidee_response_error.c b/src/libtipidee/tipidee_response_error.c
deleted file mode 100644
index 7c77e80..0000000
--- a/src/libtipidee/tipidee_response_error.c
+++ /dev/null
@@ -1,41 +0,0 @@
-/* ISC license. */
-
-#include <stddef.h>
-
-#include <skalibs/types.h>
-#include <skalibs/buffer.h>
-
-#include <tipidee/method.h>
-#include <tipidee/rql.h>
-#include <tipidee/response.h>
-
-size_t tipidee_response_error (buffer *b, tipidee_rql const *rql, unsigned int status, char const *rsl, char const *text, uint32_t options)
-{
-  size_t n = 0 ;
-  static char const txt1[] = "<html>\n<head><title>" ;
-  static char const txt2[] = "</title></head>\n<body>\n<h1> " ;
-  static char const txt3[] = " </h1>\n<p>\n" ;
-  static char const txt4[] = "\n</p>\n</body>\n</html>\n" ;
-  n += tipidee_response_status(b, rql, status, rsl) ;
-  n += tipidee_response_header_common_put_g(buffer_1, options) ;
-  if (!(options & 2))
-  {
-    char fmt[SIZE_FMT] ;
-    n += buffer_putsnoflush(buffer_1, "Content-Type: text/html; charset=UTF-8\r\n") ;
-    n += buffer_putsnoflush(buffer_1, "Content-Length: ") ;
-    n += buffer_putnoflush(buffer_1, fmt, size_fmt(fmt, sizeof(txt1) + sizeof(txt2) + sizeof(txt3) + sizeof(txt4) - 4 + 2 * strlen(rsl) + strlen(text))) ;
-    n += buffer_putnoflush(buffer_1, "\r\n", 2) ;
-  }
-  n += buffer_putnoflush(buffer_1, "\r\n", 2) ;
-  if (rql->m != TIPIDEE_METHOD_HEAD)
-  {
-    n += buffer_putsnoflush(buffer_1, txt1) ;
-    n += buffer_putsnoflush(buffer_1, rsl) ;
-    n += buffer_putsnoflush(buffer_1, txt2) ;
-    n += buffer_putsnoflush(buffer_1, rsl) ;
-    n += buffer_putsnoflush(buffer_1, txt3) ;
-    n += buffer_putsnoflush(buffer_1, text) ;
-    n += buffer_putsnoflush(buffer_1, txt4) ;
-  }
-  return n ;
-}
diff --git a/src/libtipidee/tipidee_response_error_nofile.c b/src/libtipidee/tipidee_response_error_nofile.c
new file mode 100644
index 0000000..4f628ac
--- /dev/null
+++ b/src/libtipidee/tipidee_response_error_nofile.c
@@ -0,0 +1,35 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+
+#include <tipidee/method.h>
+#include <tipidee/response.h>
+
+size_t tipidee_response_error_nofile (buffer *b, tipidee_rql const *rql, unsigned int status, char const *reason, char const *text, uint32_t options, tain const *stamp)
+{
+  static char const txt1[] = "<html>\n<head><title>" ;
+  static char const txt2[] = "</title></head>\n<body>\n<h1> " ;
+  static char const txt3[] = " </h1>\n<p>\n" ;
+  static char const txt4[] = "\n</p>\n</body>\n</html>\n" ;
+  char fmt[SIZE_FMT] ;
+  size_t n = tipidee_response_status(b, rql, status, reason) ;
+  n += tipidee_response_header_common_put(b, options, stamp) ;
+  n += buffer_putsnoflush(b, "Content-Type: text/html; charset=UTF-8\r\n") ;
+  n += buffer_putsnoflush(b, "Content-Length: ") ;
+  n += buffer_putnoflush(b, fmt, size_fmt(fmt, text ? sizeof(txt1) + sizeof(txt2) + sizeof(txt3) + sizeof(txt4) - 4 + 2 * strlen(reason) + strlen(text) : 0)) ;
+  n += buffer_putnoflush(b, "\r\n\r\n", 4) ;
+  if (text && rql->m != TIPIDEE_METHOD_HEAD)
+  {
+    n += buffer_putsnoflush(b, txt1) ;
+    n += buffer_putsnoflush(b, reason) ;
+    n += buffer_putsnoflush(b, txt2) ;
+    n += buffer_putsnoflush(b, reason) ;
+    n += buffer_putsnoflush(b, txt3) ;
+    n += buffer_putsnoflush(b, text) ;
+    n += buffer_putsnoflush(b, txt4) ;
+  }
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_response_file.c b/src/libtipidee/tipidee_response_file.c
new file mode 100644
index 0000000..0cbe8f4
--- /dev/null
+++ b/src/libtipidee/tipidee_response_file.c
@@ -0,0 +1,31 @@
+/* ISC license. */
+
+#include <stddef.h>
+
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+
+#include <tipidee/conf.h>
+#include <tipidee/method.h>
+#include <tipidee/rql.h>
+#include <tipidee/response.h>
+#include <tipidee/util.h>
+
+size_t tipidee_response_file (buffer *b, tipidee_rql const *rql, unsigned int status, char const *reason, struct stat const *st, char const *ct, uint32_t options, tain const *stamp)
+{
+  char fmt[128] ;
+  size_t n = tipidee_response_status(b, rql, status, reason) ;
+  n += tipidee_response_header_common_put(b, options & 1, stamp) ;
+  if (options & 2)
+  {
+    size_t l = tipidee_response_header_lastmodified(fmt, 128, st) ;
+    if (l) n += buffer_putnoflush(b, fmt, l) ;
+  }
+  n += buffer_putsnoflush(b, "Content-Type: ") ;
+  n += buffer_putsnoflush(b, ct) ;
+  n += buffer_putsnoflush(b, "\r\nContent-Length: ") ;
+  fmt[uint64_fmt(fmt, st->st_size)] = 0 ;
+  n += buffer_putsnoflush(b, fmt) ;
+  n += buffer_putnoflush(b, "\r\n\r\n", 4) ;
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_response_header_common_put.c b/src/libtipidee/tipidee_response_header_common_put.c
index c67cc94..e9a1a07 100644
--- a/src/libtipidee/tipidee_response_header_common_put.c
+++ b/src/libtipidee/tipidee_response_header_common_put.c
@@ -1,23 +1,20 @@
 /* ISC license. */
 
-#include <stdint.h>
-
 #include <skalibs/buffer.h>
 
-#include <tipidee/config.h>
 #include <tipidee/response.h>
 
 size_t tipidee_response_header_common_put (buffer *b, uint32_t options, tain const *stamp)
 {
   char fmt[128] ;
-  size_t m = buffer_putnoflush(b, fmt, tipidee_response_header_date(fmt, 128, stamp)) ;
+  size_t n = buffer_putnoflush(b, fmt, tipidee_response_header_date(fmt, 128, stamp)) ;
   for (tipidee_response_header_builtin const *p = tipidee_response_header_builtin_table ; p->key ; p++)
   {
-    m += buffer_putsnoflush(b, p->key) ;
-    m += buffer_putnoflush(b, ": ", 2) ;
-    m += buffer_putsnoflush(b, p->value) ;
-    m += buffer_putnoflush(b, "\r\n", 2) ;
+    n += buffer_putsnoflush(b, p->key) ;
+    n += buffer_putnoflush(b, ": ", 2) ;
+    n += buffer_putsnoflush(b, p->value) ;
+    n += buffer_putnoflush(b, "\r\n", 2) ;
   }
-  if (options & 1) m += buffer_putsnoflush(b, "Connection: close\r\n") ;
-  return m ;
+  if (options & 1) n += buffer_putsnoflush(b, "Connection: close\r\n") ;
+  return n ;
 }
diff --git a/src/libtipidee/tipidee_response_status.c b/src/libtipidee/tipidee_response_status.c
index 4315f61..3c8977a 100644
--- a/src/libtipidee/tipidee_response_status.c
+++ b/src/libtipidee/tipidee_response_status.c
@@ -5,7 +5,7 @@
 
 #include <tipidee/response.h>
 
-size_t tipidee_response_status (buffer *b, tipidee_rql const *rql, unsigned int status, char const *line)
+size_t tipidee_response_status (buffer *b, tipidee_rql const *rql, unsigned int status, char const *reason)
 {
   size_t n = 0 ;
   char fmt[UINT_FMT] ;
@@ -16,7 +16,7 @@ size_t tipidee_response_status (buffer *b, tipidee_rql const *rql, unsigned int
   n += buffer_putnoflush(b, " ", 1) ;
   n += buffer_putnoflush(b, fmt, uint_fmt(fmt, status)) ;
   n += buffer_putnoflush(b, " ", 1) ;
-  n += buffer_putsnoflush(b, line) ;
+  n += buffer_putsnoflush(b, reason) ;
   n += buffer_putnoflush(b, "\r\n", 2) ;
   return n ;
 }
diff --git a/src/libtipidee/tipidee_rql_read.c b/src/libtipidee/tipidee_rql_read.c
index fc99f37..fe6091d 100644
--- a/src/libtipidee/tipidee_rql_read.c
+++ b/src/libtipidee/tipidee_rql_read.c
@@ -66,8 +66,9 @@ static inline int get_version (char const *in, tipidee_rql *rql)
 int tipidee_rql_read (buffer *b, char *buf, size_t max, size_t *w, tipidee_rql *rql, tain const *deadline, tain *stamp)
 {
   size_t pos[3] = { 0 } ;
-  if (timed_getlnmax(b, buf, max, &pos[0], '\n', deadline, stamp) == -1)
-    return errno == ETIMEDOUT ? 99 : -1 ;
+  ssize_t r = timed_getlnmax(b, buf, max, &pos[0], '\n', deadline, stamp) ;
+  if (r == -1) return errno == ETIMEDOUT ? 99 : -1 ;
+  if (!r) return 98 ;
   buf[--pos[0]] = 0 ;
   if (buf[pos[0] - 1] == '\r') buf[--pos[0]] = 0 ;
   if (!rql_tokenize(buf, pos)) return 400 ;
diff --git a/src/tipideed/cgi.c b/src/tipideed/cgi.c
index 8dde023..0d7e104 100644
--- a/src/tipideed/cgi.c
+++ b/src/tipideed/cgi.c
@@ -21,55 +21,51 @@
 #include <skalibs/env.h>
 #include <skalibs/exec.h>
 #include <skalibs/unix-timed.h>
-#include <skalibs/lolstdio.h>
 
-#include <tipidee/method.h>
-#include <tipidee/headers.h>
-#include <tipidee/response.h>
-#include <tipidee/uri.h>
+#include <tipidee/tipidee.h>
 #include "tipideed-internal.h"
 
-static void addenv_ (tipidee_rql const *rql, char const *k, char const *v, int slash)
+static void addenv_ (tipidee_rql const *rql, char const *docroot, char const *k, char const *v, int slash)
 {
   if (!stralloc_cats(&g.sa, k)
    || !stralloc_catb(&g.sa, "=/", 1 + !!slash)
    || !stralloc_cats(&g.sa, v)
    || !stralloc_0(&g.sa))
-    die500sys(rql, 111, "stralloc_catb") ;
+    die500sys(rql, 111, docroot, "stralloc_catb") ;
 }
 
-#define addenv(rql, k, v) addenv_(rql, k, (v), 0)
-#define addenvslash(rql, k, v) addenv_(rql, k, (v), 1)
+#define addenv(rql, d, k, v) addenv_(rql, d, k, (v), 0)
+#define addenvslash(rql, d, k, v) addenv_(rql, d, k, (v), 1)
 
-static void delenv (tipidee_rql const *rql, char const *k)
+static void delenv (tipidee_rql const *rql, char const *docroot, char const *k)
 {
   if (!stralloc_cats(&g.sa, k)
    || !stralloc_0(&g.sa))
-    die500sys(rql, 111, "stralloc_catb") ;
+    die500sys(rql, 111, docroot, "stralloc_catb") ;
 }
 
-static inline void modify_env (tipidee_rql const *rql, tipidee_headers const *hdr, size_t cl, char const *script, char const *infopath)
+static inline void modify_env (tipidee_rql const *rql, char const *docroot, tipidee_headers const *hdr, size_t cl, char const *script, char const *infopath)
 {
   uint32_t got = 0 ;
-  addenv(rql, "REQUEST_METHOD", tipidee_method_tostr(rql->m)) ;
+  addenv(rql, docroot, "REQUEST_METHOD", tipidee_method_tostr(rql->m)) ;
   if (cl)
   {
     char fmt[SIZE_FMT] ;
     fmt[size_fmt(fmt, cl)] = 0 ;
-    addenv(rql, "CONTENT_LENGTH", fmt) ;
+    addenv(rql, docroot, "CONTENT_LENGTH", fmt) ;
   }
-  else delenv(rql, "CONTENT_LENGTH") ;
+  else delenv(rql, docroot, "CONTENT_LENGTH") ;
 
-  if (infopath) addenvslash(rql, "PATH_INFO", infopath) ;
-  else delenv(rql, "PATH_INFO") ;
-  if (rql->uri.query) addenv(rql, "QUERY_STRING", rql->uri.query) ;
-  else delenv(rql, "QUERY_STRING") ;
-  addenv(rql, "SCRIPT_NAME", script) ;
-  addenv(rql, "SERVER_NAME", rql->uri.host) ;
+  if (infopath) addenvslash(rql, docroot, "PATH_INFO", infopath) ;
+  else delenv(rql, docroot, "PATH_INFO") ;
+  if (rql->uri.query) addenv(rql, docroot, "QUERY_STRING", rql->uri.query) ;
+  else delenv(rql, docroot, "QUERY_STRING") ;
+  addenv(rql, docroot, "SCRIPT_NAME", script) ;
+  addenv(rql, docroot, "SERVER_NAME", rql->uri.host) ;
   {
     char proto[9] = "HTTP/1.1" ;
     if (!rql->http_minor) proto[7] = '0' ;
-    addenv(rql, "SERVER_PROTOCOL", proto) ;
+    addenv(rql, docroot, "SERVER_PROTOCOL", proto) ;
   }
   
   for (size_t i = 0 ; i < hdr->n ; i++)
@@ -84,36 +80,36 @@ static inline void modify_env (tipidee_rql const *rql, tipidee_headers const *hd
         char scheme[n] ;
         memcpy(scheme, val, n-1) ;
         scheme[n-1] = 0 ;
-        addenv(rql, "AUTH_TYPE", scheme) ;
+        addenv(rql, docroot, "AUTH_TYPE", scheme) ;
         got |= 1 ;
       }
     }
-    else if (!strcasecmp(key, "Content-Type")) { addenv(rql, "CONTENT_TYPE", val) ; got |= 2 ; }
+    else if (!strcasecmp(key, "Content-Type")) { addenv(rql, docroot, "CONTENT_TYPE", val) ; got |= 2 ; }
     else if (!strcasecmp(key, "Content-Length") || !strcasecmp(key, "Connection") || !strcasecmp(key, "Host")) ;
     else
     {
       size_t len = strlen(key), pos = g.sa.len + 5 ;
       if (!stralloc_catb(&g.sa, "HTTP_", 5)) die500sys(rql, 111, "stralloc_catb") ;
-      addenv(rql, key, val) ;
+      addenv(rql, docroot, key, val) ;
       for (char *s = g.sa.s + pos ; len-- ; s++)
         if (*s == '-') *s = '_' ;
         else if (*s >= 'a' && *s <= 'z') *s -= 32 ;
     }
   }
-  if (!(got & 1)) delenv(rql, "AUTH_TYPE") ;
-  if (!(got & 2)) delenv(rql, "CONTENT_TYPE") ;
+  if (!(got & 1)) delenv(rql, docroot, "AUTH_TYPE") ;
+  if (!(got & 2)) delenv(rql, docroot, "CONTENT_TYPE") ;
 }
 
-static inline int do_nph (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen) gccattr_noreturn ;
-static inline int do_nph (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen)
+static inline int do_nph (tipidee_rql const *rql, char const *docroot, char const *const *argv, char const *const *envp, char const *body, size_t bodylen) gccattr_noreturn ;
+static inline int do_nph (tipidee_rql const *rql, char const *docroot, char const *const *argv, char const *const *envp, char const *body, size_t bodylen)
 {
   int p[2] ;
-  if (pipe(p) == -1) die500sys(rql, 111, "pipe") ;
+  if (pipe(p) == -1) die500sys(rql, 111, docroot, "pipe") ;
   if (bodylen)
   {
     switch (fork())
     {
-      case -1 : die500sys(rql, 111, "fork") ;
+      case -1 : die500sys(rql, 111, docroot, "fork") ;
       case 0 :      
       {
 #define NAME "tipideed (nph helper for pid "
@@ -138,12 +134,12 @@ static inline int do_nph (tipidee_rql const *rql, char const *const *argv, char
     }
   }
   close(p[1]) ;
-  if (fd_move(0, p[0]) == -1) die500sys(rql, 111, "fd_move") ;
+  if (fd_move(0, p[0]) == -1) die500sys(rql, 111, docroot, "fd_move") ;
   exec_e(argv, envp) ;
-  die500sys(rql, errno == ENOENT ? 127 : 126, "exec nph ", argv[0]) ;
+  die500sys(rql, errno == ENOENT ? 127 : 126, docroot, "exec nph ", argv[0]) ;
 }
 
-static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, tipidee_headers *hdr, stralloc *sa)
+static inline int run_cgi (tipidee_rql const *rql, char const *docroot, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, tipidee_headers *hdr, stralloc *sa)
 {
   iopause_fd x[2] = { { .events = IOPAUSE_READ }, { .events = IOPAUSE_WRITE } } ;
   size_t bodyw = 0 ;
@@ -157,26 +153,25 @@ static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char
   {
     int fd[2] = { 0, 1 } ;
     pid = child_spawn2(argv[0], argv, envp, fd) ;
-    if (!pid) die500sys(rql, 111, "spawn ", argv[0]) ;
+    if (!pid) die500sys(rql, 111, docroot, "spawn ", argv[0]) ;
     x[0].fd = fd[0] ; x[1].fd = fd[1] ;
   }
   if (!bodylen)
   {
     close(x[1].fd) ;
     x[1].fd = -1 ;
-    LOLDEBUG("run_cgi: no request body, closing writing pipe to cgi") ;
   }
   buffer_init(&b, &buffer_read, x[0].fd, buf, 4096) ;
   tain_add_g(&deadline, &g.cgitto) ;
   while (x[0].fd >= 0)
   {
     int r = iopause_g(x, 1 + (x[1].fd >= 0), &deadline) ;
-    if (r == -1) die500sys(rql, 111, "iopause") ;
+    if (r == -1) die500sys(rql, 111, docroot, "iopause") ;
     if (!r)
     {
       kill(pid, SIGTERM) ;
-      respond_504(rql) ;
       strerr_warnw3x("cgi ", argv[0], " timed out") ;
+      respond_504(rql, docroot) ;
       break ;
     }
     if (x[1].fd >= 0 && x[1].revents & (IOPAUSE_WRITE | IOPAUSE_EXCEPT))
@@ -192,7 +187,6 @@ static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char
       {
         close(x[1].fd) ;
         x[1].fd = -1 ;
-        LOLDEBUG("run_cgi: finished writing body") ;
       }
     }
     if (x[0].fd >= 0 && x[0].revents & (IOPAUSE_READ | IOPAUSE_EXCEPT))
@@ -205,20 +199,20 @@ static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char
           switch (r)
           {
             case -2 : break ;
-            case -1 : die500sys(rql, 111, "read from cgi ", argv[0]) ;
+            case -1 : die500sys(rql, 111, docroot, "read from cgi ", argv[0]) ;
             case 0 :
             {
               size_t n = buffer_len(&b) ;
-              if (!stralloc_readyplus(sa, n)) die500sys(rql, 111, "stralloc_readyplus") ;
+              if (!stralloc_readyplus(sa, n)) die500sys(rql, 111, docroot, "stralloc_readyplus") ;
               buffer_getnofill(&b, sa->s + sa->len, n) ;
               sa->len += n ;
               rstate = 1 ;
               break ;
             }
-            case 400 : die502x(rql, 2, "invalid headers", " from cgi ", argv[0]) ;
-            case 413 : die502x(rql, 2, hdr->n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data", " from cgi ", argv[0]) ;
-            case 500 : die500x(rql, 101, "can't happen: ", "avltreen_insert failed", " in do_cgi") ;
-            default : die500x(rql, 101, "can't happen: ", "unknown tipidee_headers_parse return code", " in do_cgi") ;
+            case 400 : die502x(rql, 2, docroot, "invalid headers", " from cgi ", argv[0]) ;
+            case 413 : die502x(rql, 2, docroot, hdr->n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data", " from cgi ", argv[0]) ;
+            case 500 : die500x(rql, 101, docroot, "can't happen: ", "avltreen_insert failed", " in do_cgi") ;
+            default : die500x(rql, 101, docroot, "can't happen: ", "unknown tipidee_headers_parse return code", " in do_cgi") ;
           }
           if (!rstate) break ;
         }
@@ -227,13 +221,12 @@ static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char
           if (!slurpn(x[0].fd, sa, g.maxcgibody))
           {
             if (error_isagain(errno)) break ;
-            else if (errno == ENOBUFS) die502x(rql, 2, "Too fat body", " from cgi ", argv[0]) ;
-            else die500sys(rql, 111, "read body", " from cgi ", argv[0]) ;
+            else if (errno == ENOBUFS) die502x(rql, 2, docroot, "Too fat body", " from cgi ", argv[0]) ;
+            else die500sys(rql, 111, docroot, "read body", " from cgi ", argv[0]) ;
           }
           close(x[0].fd) ;
           x[0].fd = -1 ;
           rstate = 2 ;
-          LOLDEBUG("run_cgi: rstate = 2") ;
         }
       }
     }
@@ -243,7 +236,7 @@ static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char
   return rstate == 2 ;
 }
 
-static inline int local_redirect (tipidee_rql *rql, char const *loc, char *uribuf, char const *cginame)
+static inline int local_redirect (tipidee_rql *rql, char const *docroot, char const *loc, char *uribuf, char const *cginame)
 {
   size_t n ;
   size_t hostlen = strlen(rql->uri.host) ;
@@ -253,7 +246,7 @@ static inline int local_redirect (tipidee_rql *rql, char const *loc, char *uribu
   memcpy(hosttmp, rql->uri.host, hostlen + 1) ;
   n = tipidee_uri_parse(uribuf, URI_BUFSIZE, loc, &rql->uri) ;
   if (!n || n + hostlen + 1 > URI_BUFSIZE)
-    die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Location", " value", " for local redirection") ;
+    die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Location", " value", " for local redirection") ;
   memcpy(uribuf + n, hosttmp, hostlen + 1) ;
   rql->uri.host = uribuf + n ;
   rql->uri.port = port ;
@@ -292,11 +285,11 @@ static inline void print_cgi_headers (tipidee_headers const *hdr, size_t rbodyle
   buffer_putnoflush(buffer_1, "\r\n", 2) ;
 }
 
-static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *hdr, char const *rbody, size_t rbodylen, char *uribuf, char const *cginame)
+static inline int process_cgi_output (tipidee_rql *rql, char const *docroot, tipidee_headers const *hdr, char const *rbody, size_t rbodylen, char *uribuf, char const *cginame)
 {
   char const *location = tipidee_headers_search(hdr, "Location") ;
   char const *x = tipidee_headers_search(hdr, "Status") ;
-  char const *reason_phrase = "OK" ;
+  char const *reason = "OK" ;
   unsigned int status = 0 ;
   tain deadline ;
   tain_add_g(&deadline, &g.writetto) ;
@@ -304,23 +297,28 @@ static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *h
   {
     size_t m = uint_scan(x, &status) ;
     if (!m || (x[m] && x[m] != ' '))
-      die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Status", " header") ;
-    reason_phrase = x[m] ? x + m + 1 : "" ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Status", " header") ;
+    if (x[m]) reason = x + m + 1 ;
+    else
+    {
+      tipidee_defaulttext dt ;
+      reason = tipidee_util_defaulttext(status, &dt) ? dt.reason : "" ;
+    }
     if (!location && (status == 301 || status == 302 || status == 307 || status == 308))
-      die502x(rql, 2, "cgi ", cginame, " returned a redirection status code without a ", "Location", " header") ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " returned a redirection status code without a ", "Location", " header") ;
     if (status < 100 || status > 999)
-      die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Status", " value") ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Status", " value") ;
   }
   if (location)
   {
-    if (!location[0]) die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Location", " header") ;
-    if (location[0] == '/' && location[1] != '/') return local_redirect(rql, location, uribuf, cginame) ;
+    if (!location[0]) die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Location", " header") ;
+    if (location[0] == '/' && location[1] != '/') return local_redirect(rql, docroot, location, uribuf, cginame) ;
     if (rbodylen)
     {
       if (!status)
-        die502x(rql, 2, "cgi ", cginame, " didn't output a ", "Status", " header", " for a client redirect response with document") ;
+        die502x(rql, 2, docroot, "cgi ", cginame, " didn't output a ", "Status", " header", " for a client redirect response with document") ;
       if (status < 300 || status > 399)
-        die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Status", " value", " for a client redirect response with document") ;
+        die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Status", " value", " for a client redirect response with document") ;
     }
     else
     {
@@ -329,12 +327,12 @@ static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *h
         char const *key = hdr->buf + hdr->list[i].left ;
         if (!strcasecmp(key, "Location") || !strcasecmp(key, "Status")) continue ;
         if (str_start(key, "X-CGI-")) continue ;
-        die502x(rql, 2, "cgi ", cginame, " returned extra headers", " for a client redirect response without document") ;
+        die502x(rql, 2, docroot, "cgi ", cginame, " returned extra headers", " for a client redirect response without document") ;
       }
       if (!status)
       {
         status = 302 ;
-        reason_phrase = "Found" ;
+        reason = "Found" ;
       }
     }
   }
@@ -342,19 +340,19 @@ static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *h
   {
     if (!status) status = 200 ;
     if (!tipidee_headers_search(hdr, "Content-Type"))
-      die502x(rql, 2, "cgi ", cginame, " didn't output a ", "Content-Type", " header") ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " didn't output a ", "Content-Type", " header") ;
   }
   x = tipidee_headers_search(hdr, "Content-Length") ;
   if (x)
   {
     size_t cln ;
     if (!size0_scan(x, &cln))
-      die502x(rql, 2, "cgi ", cginame, " returned an invalid ", "Content-Length", " header") ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " returned an invalid ", "Content-Length", " header") ;
     if (cln != rbodylen)
-      die502x(rql, 2, "cgi ", cginame, " returned a mismatching ", "Content-Length", " header") ;
+      die502x(rql, 2, docroot, "cgi ", cginame, " returned a mismatching ", "Content-Length", " header") ;
   }
 
-  tipidee_response_status(buffer_1, rql, status, reason_phrase) ;
+  tipidee_response_status(buffer_1, rql, status, reason) ;
   tipidee_response_header_common_put_g(buffer_1, !g.cont) ;
   print_cgi_headers(hdr, rbodylen) ;
   if (buffer_timed_put_g(buffer_1, "\r\n", 2, &deadline) < 2)
@@ -370,26 +368,26 @@ static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *h
   return 0 ;
 }
 
-static inline int do_cgi (tipidee_rql *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, char *uribuf)
+static inline int do_cgi (tipidee_rql *rql, char const *docroot, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, char *uribuf)
 {
   static stralloc sa = STRALLOC_ZERO ;
   tipidee_headers hdr ;
   char hdrbuf[2048] ;
   sa.len = 0 ;
   tipidee_headers_init(&hdr, hdrbuf, 2048) ;
-  if (!run_cgi(rql, argv, envp, body, bodylen, &hdr, &sa)) return 0 ;
-  return process_cgi_output(rql, &hdr, sa.s, sa.len, uribuf, argv[0]) ;
+  if (!run_cgi(rql, docroot, argv, envp, body, bodylen, &hdr, &sa)) return 0 ;
+  return process_cgi_output(rql, docroot, &hdr, sa.s, sa.len, uribuf, argv[0]) ;
 }
 
-int respond_cgi (tipidee_rql *rql, char const *fn, size_t docrootlen, char const *infopath, char *uribuf, tipidee_headers const *hdr, tipidee_resattr const *ra, char const *body, size_t bodylen)
+int respond_cgi (tipidee_rql *rql, char const *docroot, char const *fn, size_t docrootlen, char const *infopath, char *uribuf, tipidee_headers const *hdr, tipidee_resattr const *ra, char const *body, size_t bodylen)
 {
   size_t sabase = g.sa.len ;
   size_t envmax = g.envlen + 16 + TIPIDEE_HEADERS_MAX ;
   char const *argv[2] = { fn, 0 } ;
   char const *envp[envmax] ;
-  modify_env(rql, hdr, bodylen, fn + docrootlen, infopath) ;
+  modify_env(rql, docroot, hdr, bodylen, fn + docrootlen, infopath) ;
   env_merge(envp, envmax, (char const *const *)environ, g.envlen, g.sa.s + g.cwdlen + 1, g.sa.len - (g.cwdlen+1)) ;
   g.sa.len = sabase ;
-  return ra->isnph ? do_nph(rql, argv, envp, body, bodylen) :
-                     do_cgi(rql, argv, envp, body, bodylen, uribuf) ;
+  return ra->isnph ? do_nph(rql, docroot, argv, envp, body, bodylen) :
+                     do_cgi(rql, docroot, argv, envp, body, bodylen, uribuf) ;
 }
diff --git a/src/tipideed/regular.c b/src/tipideed/regular.c
index 845f12b..aa937ad 100644
--- a/src/tipideed/regular.c
+++ b/src/tipideed/regular.c
@@ -4,7 +4,6 @@
 
 #include <errno.h>
 
-#include <skalibs/uint64.h>
 #include <skalibs/stat.h>
 #include <skalibs/types.h>
 #include <skalibs/buffer.h>
@@ -18,24 +17,12 @@
 #include <tipidee/log.h>
 #include "tipideed-internal.h"
 
-int respond_regular (tipidee_rql const *rql, char const *fn, struct stat const *st, tipidee_resattr const *ra)
+int respond_regular (tipidee_rql const *rql, char const *docroot, char const *fn, struct stat const *st, tipidee_resattr const *ra)
 {
-  tain deadline ;
-  char fmt[128] ;
-  size_t n = tipidee_response_status(buffer_1, rql, 200, "OK") ;
-  n += tipidee_response_header_common_put_g(buffer_1, !g.cont) ;
-  {
-    size_t l = tipidee_response_header_lastmodified(fmt, 128, st) ;
-    if (l) n += buffer_putnoflush(buffer_1, fmt, l) ;
-  }
-  n += buffer_putsnoflush(buffer_1, "Content-Type: ") ;
-  n += buffer_putsnoflush(buffer_1, ra->content_type) ;
-  n += buffer_putsnoflush(buffer_1, "\r\nContent-Length: ") ;
-  fmt[uint64_fmt(fmt, st->st_size)] = 0 ;
-  n += buffer_putsnoflush(buffer_1, fmt) ;
-  n += buffer_putnoflush(buffer_1, "\r\n\r\n", 4) ;
   if (rql->m == TIPIDEE_METHOD_HEAD)
   {
+    tain deadline ;
+    tipidee_response_file_g(buffer_1, rql, 200, "OK", st, ra->content_type, 2 | !g.cont) ;
     tipidee_log_answer(g.logv, rql, 200, st->st_size) ;
     tain_add_g(&deadline, &g.writetto) ;
     if (!buffer_timed_flush_g(buffer_1, &deadline))
@@ -46,14 +33,14 @@ int respond_regular (tipidee_rql const *rql, char const *fn, struct stat const *
     int fd = open_read(fn) ;
     if (fd == -1)
     {
-      buffer_unput(buffer_1, n) ;
       if (errno == EACCES)
       {
-        respond_403(rql) ;
+        respond_403(rql, docroot) ;
         return 0 ;
       }
-      else die500sys(rql, 111, "open ", fn) ;
+      else die500sys(rql, 111, docroot, "open ", fn) ;
     }
+    tipidee_response_file_g(buffer_1, rql, 200, "OK", st, ra->content_type, 2 | !g.cont) ;
     tipidee_log_answer(g.logv, rql, 200, st->st_size) ;
     send_file(fd, st->st_size, fn) ;
     fd_close(fd) ;
diff --git a/src/tipideed/responses.c b/src/tipideed/responses.c
index e5706e2..0d0840c 100644
--- a/src/tipideed/responses.c
+++ b/src/tipideed/responses.c
@@ -1,44 +1,99 @@
 /* ISC license. */
 
+#include <skalibs/bsdsnowflake.h>
+
 #include <unistd.h>
 
+#include <skalibs/stat.h>
 #include <skalibs/types.h>
 #include <skalibs/buffer.h>
 #include <skalibs/strerr.h>
 #include <skalibs/tai.h>
+#include <skalibs/djbunix.h>
 #include <skalibs/unix-timed.h>
 
-#include <tipidee/rql.h>
 #include <tipidee/log.h>
+#include <tipidee/util.h>
 #include <tipidee/response.h>
 
 #include "tipideed-internal.h"
 
-void response_error (tipidee_rql const *rql, unsigned int status, char const *rsl, char const *text, uint32_t options)
+void response_error_early (tipidee_rql const *rql, unsigned int status, char const *reason, char const *text, uint32_t options)
 {
   tain deadline ;
-  tipidee_response_error(buffer_1, rql, status, rsl, text, options & 1 || !g.cont) ;
+  tipidee_response_error_nofile_g(buffer_1, rql, status, reason, text, options & 1 || !g.cont) ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+}
+
+void response_error_early_and_exit (tipidee_rql const *rql, unsigned int status, char const *reason, char const *text)
+{
+  response_error_early(rql, status, reason, text, 1) ;
+  log_and_exit(1) ;
+}
+
+void response_error (tipidee_rql const *rql, char const *docroot, unsigned int status, uint32_t options)
+{
+  tain deadline ;
+  tipidee_defaulttext dt ;
+  char const *file = tipidee_conf_get_errorfile(&g.conf, docroot, status) ;
+  if (!tipidee_util_defaulttext(status, &dt))
+  {
+    char fmt[UINT_FMT] ;
+    fmt[uint_fmt(fmt, status)] = 0 ;
+    strerr_dief2x(101, "can't happen: unknown response code ", fmt) ;
+  }
+
+  if (file)
+  {
+    int fd = open_read(file) ;
+    if (fd == -1) strerr_warnwu3sys("open ", "custom error file ", file) ;
+    else
+    {
+      struct stat st ;
+      if (fstat(fd, &st) == -1)
+      {
+        fd_close(fd) ;
+        strerr_warnwu3sys("stat ", "custom error file ", file) ;
+      }
+      else if (!S_ISREG(st.st_mode))
+      {
+        fd_close(fd) ;
+        strerr_warnw3x("custom error file ", file, " is not a regular file") ;
+      }
+      else
+      {
+        tipidee_response_file_g(buffer_1, rql, status, dt.reason, &st, tipidee_conf_get_content_type(&g.conf, file), options) ;
+        tipidee_log_answer(g.logv, rql, status, st.st_size) ;
+        send_file(fd, st.st_size, file) ;
+        fd_close(fd) ;
+        return ;
+      }
+    }
+  }
+
+  tipidee_response_error_nofile_g(buffer_1, rql, status, dt.reason, dt.text, options & 1 || !g.cont) ;
+  tipidee_log_answer(g.logv, rql, status, 0) ;
   tain_add_g(&deadline, &g.writetto) ;
-  if (!(options & 2)) tipidee_log_answer(g.logv, rql, status, 0) ;
   if (!buffer_timed_flush_g(buffer_1, &deadline))
     strerr_diefu1sys(111, "write to stdout") ;
 }
 
-void response_error_and_exit (tipidee_rql const *rql, unsigned int status, char const *rsl, char const *text, uint32_t options)
+void response_error_and_exit (tipidee_rql const *rql, char const *docroot, unsigned int status)
 {
-  response_error(rql, status, rsl, text, options | 1) ;
-  tipidee_log_exit(g.logv, 0) ;
-  _exit(0) ;
+  response_error(rql, docroot, status, 1) ;
+  log_and_exit(0) ;
 }
 
-void response_error_and_die (tipidee_rql const *rql, int e, unsigned int status, char const *rsl, char const *text, char const *const *v, unsigned int n, uint32_t options)
+void response_error_and_die (tipidee_rql const *rql, int e, char const *docroot, unsigned int status, char const *const *v, unsigned int n, uint32_t options)
 {
-  response_error(rql, status, rsl, text, options | 1) ;
+  response_error(rql, docroot, status, options | 1) ;
   if (options & 1) strerr_dievsys(e, v, n) ;
   else strerr_diev(e, v, n) ;
 }
 
-void exit_405 (tipidee_rql const *rql, uint32_t options)
+void exit_405_ (tipidee_rql const *rql, uint32_t options)
 {
   tain deadline ;
   tipidee_response_status(buffer_1, rql, 405, "Method Not Allowed") ;
@@ -50,8 +105,7 @@ void exit_405 (tipidee_rql const *rql, uint32_t options)
   tain_add_g(&deadline, &g.writetto) ;
   if (!buffer_timed_flush_g(buffer_1, &deadline))
     strerr_diefu1sys(111, "write to stdout") ;
-  tipidee_log_exit(g.logv, 0) ;
-  _exit(0) ;
+  log_and_exit(0) ;
 }
 
 void respond_30x (tipidee_rql const *rql, tipidee_redirection const *rd)
diff --git a/src/tipideed/tipideed-internal.h b/src/tipideed/tipideed-internal.h
index fe01152..a4f6d95 100644
--- a/src/tipideed/tipideed-internal.h
+++ b/src/tipideed/tipideed-internal.h
@@ -71,42 +71,38 @@ extern void tipideed_harden (unsigned int) ;
 
  /* Responses */
 
-extern void response_error (tipidee_rql const *, unsigned int, char const *, char const *, uint32_t) ;  /* set bit 0 for Connection: close, bit 1 for preexit */
-extern void response_error_and_exit (tipidee_rql const *, unsigned int, char const *, char const *, uint32_t) gccattr_noreturn ;
-extern void response_error_and_die (tipidee_rql const *, int e, unsigned int, char const *, char const *, char const *const *, unsigned int, uint32_t) gccattr_noreturn ;
-
- /*
-    preexit is meant to be called before tipidee_log_request(), it won't log an answer line.
-    Use for early parsing, when the request isn't even validated yet.
-    exit is meant to be called after tipidee_log_request(), it will log an answer line.
-    respond can only happen after tipidee_log_request(), it will log an answer line.
-
-    exit will log an informational exit line.
-    die will not; there will be a fatal line instead.
-    die will log an answer line. There is no "predie", we just use strerr_die instead.
- */
-
-#define preexit_400(r, s) response_error_and_exit(r, 400, "Bad Request", s, 2)
-#define exit_400(r, s) response_error_and_exit(r, 400, "Bad Request", s, 0)
-extern void exit_405 (tipidee_rql const *, uint32_t) gccattr_noreturn ;  /* set bit 0 for Allow: POST, bit 1 for preexit */
-#define preexit_408(r) response_error_and_exit(r, 408, "Request Timeout", "", 2)
-#define exit_408(r) response_error_and_exit(r, 408, "Request Timeout", "", 0)
-#define preexit_413(r, s) response_error_and_exit(r, 413, "Request Entity Too Large", s, 2)
-#define exit_413(r, s) response_error_and_exit(r, 413, "Request Entity Too Large", s, 0)
-#define preexit_501(r, s) response_error_and_exit(r, 501, "Not Implemented", s, 2)
-#define exit_501(r, s) response_error_and_exit(r, 501, "Not Implemented", s, 0)
-
-#define respond_403(r) response_error(r, 403, "Forbidden", "Missing credentials to access the URI.", 0)
-#define respond_404(r) response_error(r, 404, "Not Found", "The request URI was not found.", 0)
-#define respond_414(r) response_error(r, 414, "URI Too Long", "The request URI had an oversized component.", 0)
+extern void response_error_early (tipidee_rql const *, unsigned int, char const *, char const *, uint32_t) ;  /* set bit 0 for Connection: close */
+extern void response_error_early_and_exit (tipidee_rql const *, unsigned int, char const *, char const *) gccattr_noreturn ;
+extern void exit_405_ (tipidee_rql const *, uint32_t) gccattr_noreturn ;  /* set bit 0 for Allow: POST, bit 1 for preexit */
+
+#define eexit_400(r, s) response_error_early_and_exit(r, 400, "Bad Request", s)
+#define eexit_405(r) exit_405_((r), 3)
+#define eexit_408(r) response_error_early_and_exit((r), 408, "Request Timeout", 0)
+#define eexit_413(r, s) response_error_early_and_exit(r, 413, "Request Entity Too Large", s)
+#define eexit_501(r, s) response_error_early_and_exit(r, 501, "Not Implemented", s)
+
+
+extern void response_error (tipidee_rql const *, char const *, unsigned int, uint32_t) ;  /* set bit 0 for Connection: close */
+extern void response_error_and_exit (tipidee_rql const *, char const *, unsigned int) gccattr_noreturn ;
+extern void response_error_and_die (tipidee_rql const *, int, char const *, unsigned int, char const *const *, unsigned int, uint32_t) gccattr_noreturn ;  /* set bit 0 for diesys */
+
+#define exit_400(r, d) response_error_and_exit(r, (d), 400)
+#define exit_405(r) exit_405_((r), 0)
+#define exit_408(r, d) response_error_and_exit(r, (d), 408)
+#define exit_413(r, d) response_error_and_exit(r, (d), 413)
+#define exit_501(r, d) response_error_and_exit(r, (d), 501)
+
 extern void respond_30x (tipidee_rql const *, tipidee_redirection const *) ;
-#define respond_504(r) response_error(r, 504, "Gateway Timeout", "The CGI script took too long to answer.", 0)
+#define respond_403(r, d) response_error(r, (d), 403, 0)
+#define respond_404(r, d) response_error(r, (d), 404, 0)
+#define respond_414(r, d) response_error(r, (d), 414, 0)
+#define respond_504(r, d) response_error(r, (d), 504, 0)
 
-#define diefx(r, e, status, rsl, text, ...) response_error_and_die(r, e, status, rsl, text, strerr_array(PROG, ": fatal: ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+2, 0)
-#define diefusys(r, e, status, rsl, text, ...) response_error_and_die(r, e, status, rsl, text, strerr_array(PROG, ": fatal: ", "unable to ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+3, 1)
-#define die500x(r, e, ...) diefx(r, e, 500, "Internal Server Error", "Bad server configuration.", __VA_ARGS__)
-#define die500sys(r, e, ...) diefusys(r, e, 500, "Internal Server Error", "System error.", __VA_ARGS__)
-#define die502x(r, e, ...) diefx(r, e, 502, "Bad Gateway", "Bad CGI script.", __VA_ARGS__)
+#define diefx(r, e, d, status, ...) response_error_and_die(r, e, d, status, strerr_array(PROG, ": fatal: ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+2, 0)
+#define diefusys(r, e, d, status, ...) response_error_and_die(r, e, d, status, strerr_array(PROG, ": fatal: ", "unable to ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+3, 1)
+#define die500x(r, e, d, ...) diefx(r, e, d, 500, __VA_ARGS__)
+#define die500sys(r, e, d, ...) diefusys(r, e, d, 500, __VA_ARGS__)
+#define die502x(r, e, d, ...) diefx(r, e, d, 502, __VA_ARGS__)
 
 
  /* Trace */
@@ -127,12 +123,17 @@ extern void send_file (int, uint64_t, char const *) ;
 
  /* regular */
 
-extern int respond_regular (tipidee_rql const *, char const *, struct stat const *, tipidee_resattr const *) ;
+extern int respond_regular (tipidee_rql const *, char const *, char const *, struct stat const *, tipidee_resattr const *) ;
 extern int respond_304 (tipidee_rql const *, char const *, struct stat const *) ;
 
 
  /* cgi */
 
-extern int respond_cgi (tipidee_rql *, char const *, size_t, char const *, char *, tipidee_headers const *, tipidee_resattr const *, char const *, size_t) ;
+extern int respond_cgi (tipidee_rql *, char const *, char const *, size_t, char const *, char *, tipidee_headers const *, tipidee_resattr const *, char const *, size_t) ;
+
+
+ /* main */
+
+extern void log_and_exit (int) gccattr_noreturn ;
 
 #endif
diff --git a/src/tipideed/tipideed.c b/src/tipideed/tipideed.c
index fb5ede4..b004782 100644
--- a/src/tipideed/tipideed.c
+++ b/src/tipideed/tipideed.c
@@ -45,7 +45,7 @@ static void sigchld_handler (int sig)
   wait_reap() ;
 }
 
-static inline void log_and_exit (int e)
+void log_and_exit (int e)
 {
   tipidee_log_exit(g.logv, e) ;
   _exit(e) ;
@@ -164,7 +164,7 @@ static void inittto (tain *tto, char const *key)
   else *tto = tain_infinite_relative ;
 }
 
-static inline unsigned int indexify (tipidee_rql const *rql, char *s, struct stat *st)
+static inline unsigned int indexify (tipidee_rql const *rql, char const *docroot, char *s, struct stat *st)
 {
   unsigned int e = 0 ;
   size_t len = strlen(s) ;
@@ -184,22 +184,22 @@ static inline unsigned int indexify (tipidee_rql const *rql, char *s, struct sta
       case ENAMETOOLONG : return 414 ;
       case ENOTDIR : return 404 ;
       case ENOENT : continue ;
-      default : die500sys(rql, 111, "stat ", s) ;
+      default : die500sys(rql, 111, docroot, "stat ", s) ;
     }
   }
   if (i >= g.indexn) return 404 ;
-  if (S_ISDIR(st->st_mode)) die500x(rql, 103, "bad document hierarchy: ", s, " is a directory") ;
+  if (S_ISDIR(st->st_mode)) die500x(rql, 102, docroot, "bad document hierarchy: ", s, " is a directory") ;
   if (e == 308) s[len] = 0 ;
   return e ;
 }
 
-static inline void get_resattr (tipidee_rql const *rql, char const *res, tipidee_resattr *ra)
+static inline void get_resattr (tipidee_rql const *rql, char const *docroot, char const *res, tipidee_resattr *ra)
 {
   static stralloc sa = STRALLOC_ZERO ;
   sa.len = 0 ;
-  if (sarealpath(&sa, res) == -1 || !stralloc_0(&sa)) die500sys(rql, 111, "realpath ", res) ;
+  if (sarealpath(&sa, res) == -1 || !stralloc_0(&sa)) die500sys(rql, 111, docroot, "realpath ", res) ;
   if (strncmp(sa.s, g.sa.s, g.cwdlen) || sa.s[g.cwdlen] != '/')
-    die500x(rql, 102, "resource ", res, " points outside of the server's root") ;
+    die500x(rql, 102, docroot, "resource ", res, " points outside of the server's root") ;
 
   {
     char const *attr = 0 ;
@@ -211,7 +211,7 @@ static inline void get_resattr (tipidee_rql const *rql, char const *res, tipidee
     errno = ENOENT ;
     while (!attr)
     {
-      if (errno != ENOENT) die500x(rql, 102, "invalid configuration data for ", key) ;
+      if (errno != ENOENT) die500x(rql, 102, docroot, "invalid configuration data for ", key) ;
       while (len > 2 && key[len] != '/') len-- ;
       if (len <= 2) break ;
       key[len--] = 0 ;
@@ -220,7 +220,7 @@ static inline void get_resattr (tipidee_rql const *rql, char const *res, tipidee
     }
     if (attr)
     {
-      if (*attr < '@' || *attr > 'G') die500x(rql, 102, "invalid configuration data for ", key) ;
+      if (*attr < '@' || *attr > 'G') die500x(rql, 102, docroot, "invalid configuration data for ", key) ;
       ra->iscgi = *attr & ~'@' & 1 ;
       if (attr[1]) ra->content_type = attr + 1 ;
       if (ra->iscgi)
@@ -243,7 +243,7 @@ static inline void get_resattr (tipidee_rql const *rql, char const *res, tipidee
   if (!ra->iscgi && !ra->content_type)
   {
     ra->content_type = tipidee_conf_get_content_type(&g.conf, sa.s + g.cwdlen) ;
-    if (!ra->content_type) die500sys(rql, 111, "get content type for ", sa.s + g.cwdlen) ;
+    if (!ra->content_type) die500sys(rql, 111, docroot, "get content type for ", sa.s + g.cwdlen) ;
   }
 }
 
@@ -276,7 +276,7 @@ static inline int serve (tipidee_rql *rql, char const *docroot, char *uribuf, ti
   {
     tipidee_redirection rd = TIPIDEE_REDIRECTION_ZERO ;
     int e = tipidee_conf_get_redirection(&g.conf, docroot, docrootlen, rql->uri.path, &rd) ;
-    if (e == -1) die500sys(rql, 111, "get redirection data for ", fn) ;
+    if (e == -1) die500sys(rql, 111, docroot, "get redirection data for ", fn) ;
     if (e)
     {
       respond_30x(rql, &rd) ;
@@ -292,49 +292,49 @@ static inline int serve (tipidee_rql *rql, char const *docroot, char *uribuf, ti
     for (;;)
     {
       while (fn[pos] != '/') pos-- ;
-      if (pos <= docrootlen) { respond_404(rql) ; return 0 ; }
+      if (pos <= docrootlen) { respond_404(rql, docroot) ; return 0 ; }
       fn[pos] = 0 ;
       if (stat(fn, &st) == 0) break ;
       switch (errno)
       {
         case ENOTDIR :
         case ENOENT : fn[pos--] = '/' ; break ;
-        case EACCES : respond_403(rql) ; return 0 ;
-        case ENAMETOOLONG : respond_414(rql) ; return 0 ;
-        default : die500sys(rql, 111, "stat ", fn) ;
+        case EACCES : respond_403(rql, docroot) ; return 0 ;
+        case ENAMETOOLONG : respond_414(rql, docroot) ; return 0 ;
+        default : die500sys(rql, 111, docroot, "stat ", fn) ;
       }
     }
     infopath = fn + pos + 1 ;
   }
   if (S_ISDIR(st.st_mode))
   {
-    if (infopath) { respond_404(rql) ; return 0 ; }
-    switch (indexify(rql, fn, &st))
+    if (infopath) { respond_404(rql, docroot) ; return 0 ; }
+    switch (indexify(rql, docroot, fn, &st))
     {
-      case 403 : respond_403(rql) ; return 0 ;
-      case 404 : respond_404(rql) ; return 0 ;
-      case 414 : respond_414(rql) ; return 0 ;
+      case 403 : respond_403(rql, docroot) ; return 0 ;
+      case 404 : respond_404(rql, docroot) ; return 0 ;
+      case 414 : respond_414(rql, docroot) ; return 0 ;
       case 308 : force_redirect(rql, fn) ; return 0 ;
       case 0 : break ;
     }
   }
-  LOLDEBUG("serve: %s with %s %s, docroot %s", fn, infopath ? "infopath" : "no", infopath ? infopath : "infopath", docroot) ;
+  tipidee_log_debug(g.logv, "serve: docroot ", docroot, " file ", fn, " infopath ", infopath ? infopath : "(none)") ;
 
-  get_resattr(rql, fn, &ra) ;
+  get_resattr(rql, docroot, fn, &ra) ;
 
   if (!ra.iscgi)
   {
-    if (infopath) { respond_404(rql) ; return 0 ; }
-    if (rql->m == TIPIDEE_METHOD_POST) exit_405(rql, 0) ;
+    if (infopath) { respond_404(rql, docroot) ; return 0 ; }
+    if (rql->m == TIPIDEE_METHOD_POST) exit_405(rql) ;
   }
 
   if (rql->m == TIPIDEE_METHOD_OPTIONS)
     return respond_options(rql, 2 | ra.iscgi) ;
 
-  tipidee_log_resource(g.logv, rql, docroot, fn, &ra) ;
+  tipidee_log_resource(g.logv, rql, fn, &ra, infopath) ;
 
   if (ra.iscgi)
-    return respond_cgi(rql, fn, docrootlen, infopath, uribuf, hdr, &ra, body, bodylen) ;
+    return respond_cgi(rql, docroot, fn, docrootlen, infopath, uribuf, hdr, &ra, body, bodylen) ;
 
   infopath = tipidee_headers_search(hdr, "If-Modified-Since") ;
   if (infopath)
@@ -345,7 +345,7 @@ static inline int serve (tipidee_rql *rql, char const *docroot, char *uribuf, ti
      && tain_less(&actual, &wanted))
       return respond_304(rql, fn, &st) ;
   }
-  return respond_regular(rql, fn, &st, &ra) ;
+  return respond_regular(rql, docroot, fn, &st, &ra) ;
 }
 
 int main (int argc, char const *const *argv, char const *const *envp)
@@ -403,6 +403,8 @@ int main (int argc, char const *const *argv, char const *const *envp)
     init_splice_pipe() ;
     if (!sig_catch(SIGCHLD, &sigchld_handler))
       strerr_diefu1sys(111, "set SIGCHLD handler") ;
+    if (!sig_altignore(SIGPIPE))
+      strerr_diefu1sys(111, "ignore SIGPIPE") ;
     if (!tain_now_set_stopwatch_g())
       strerr_diefu1sys(111, "initialize clock") ;
     tipidee_log_start(g.logv, g.sa.s + remoteip, g.sa.s + remotehost) ;
@@ -430,14 +432,15 @@ int main (int argc, char const *const *argv, char const *const *envp)
     e = tipidee_rql_read_g(buffer_0, uribuf, URI_BUFSIZE, &content_length, &rql, &deadline) ;
     switch (e)
     {
-      case -1 : log_and_exit(1) ;  /* Malicious or shitty client */
+      case -1 : log_and_exit(1) ;  /* bad client */
       case 0 : break ;
-      case 99 : g.cont = 0 ; continue ;  /* timeout, it's ok */
-      case 400 : preexit_400(&rql, "Syntax error in request line") ;
+      case 98 :  /* client exited */
+      case 99 : g.cont = 0 ; continue ;  /* timeout */
+      case 400 : eexit_400(&rql, "Syntax error in request line") ;
       default : strerr_dief2x(101, "can't happen: ", "unknown tipidee_rql_read return code") ;
     }
     if (rql.http_major != 1) log_and_exit(1) ;
-    if (rql.http_minor > 1) preexit_400(&rql, "Bad HTTP version") ;
+    if (rql.http_minor > 1) eexit_400(&rql, "Bad HTTP version") ;
 
     content_length = 0 ;
     tipidee_headers_init(&hdr, hdrbuf, HDR_BUFSIZE) ;
@@ -446,9 +449,9 @@ int main (int argc, char const *const *argv, char const *const *envp)
     {
       case -1 : log_and_exit(1) ;  /* connection issue, client timeout, etc. */
       case 0 : break ;
-      case 400 : preexit_400(&rql, "Syntax error in headers") ;
-      case 408 : preexit_408(&rql) ;  /* timeout */
-      case 413 : preexit_413(&rql, hdr.n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data") ;
+      case 400 : eexit_400(&rql, "Syntax error in headers") ;
+      case 408 : eexit_408(&rql) ;  /* timeout */
+      case 413 : eexit_413(&rql, hdr.n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data") ;
       case 500 : strerr_dief2x(101, "can't happen: ", "avltreen_insert failed") ;
       default : strerr_dief2x(101, "can't happen: ", "unknown tipidee_headers_parse return code") ;
     }
@@ -467,7 +470,7 @@ int main (int argc, char const *const *argv, char const *const *envp)
     x = tipidee_headers_search(&hdr, "Transfer-Encoding") ;
     if (x)
     {
-      if (strcasecmp(x, "chunked")) preexit_400(&rql, "unsupported Transfer-Encoding") ;
+      if (strcasecmp(x, "chunked")) eexit_400(&rql, "unsupported Transfer-Encoding") ;
       else tcoding = TIPIDEE_TRANSFERCODING_CHUNKED ;
     }
     else
@@ -475,7 +478,7 @@ int main (int argc, char const *const *argv, char const *const *envp)
       x = tipidee_headers_search(&hdr, "Content-Length") ;
       if (x)
       {
-        if (!size_scan(x, &content_length)) preexit_400(&rql, "Invalid Content-Length") ;
+        if (!size_scan(x, &content_length)) eexit_400(&rql, "Invalid Content-Length") ;
         else if (content_length) tcoding = TIPIDEE_TRANSFERCODING_FIXED ;
         else tcoding = TIPIDEE_TRANSFERCODING_NONE ;
       }
@@ -483,7 +486,7 @@ int main (int argc, char const *const *argv, char const *const *envp)
     }
 
     if (tcoding != TIPIDEE_TRANSFERCODING_NONE && rql.m != TIPIDEE_METHOD_POST)
-      preexit_400(&rql, "only POST requests can have an entity body") ;
+      eexit_400(&rql, "only POST requests can have an entity body") ;
 
     switch (rql.m)
     {
@@ -495,9 +498,9 @@ int main (int argc, char const *const *argv, char const *const *envp)
         if (!rql.uri.path) { respond_options(&rql, 1) ; continue ; }
         break ;
       case TIPIDEE_METHOD_PUT :
-      case TIPIDEE_METHOD_DELETE : exit_405(&rql, 3) ;
-      case TIPIDEE_METHOD_CONNECT : preexit_501(&rql, "CONNECT method unsupported") ;
-      case TIPIDEE_METHOD_PRI : preexit_501(&rql, "PRI method attempted with HTTP version 1") ;
+      case TIPIDEE_METHOD_DELETE : eexit_405(&rql) ;
+      case TIPIDEE_METHOD_CONNECT : eexit_501(&rql, "CONNECT method unsupported") ;
+      case TIPIDEE_METHOD_PRI : eexit_501(&rql, "PRI method attempted with HTTP version 1") ;
       default : strerr_dief2x(101, "can't happen: ", "unknown HTTP method") ;
     }
 
@@ -509,13 +512,13 @@ int main (int argc, char const *const *argv, char const *const *envp)
         char *p = strchr(x, ':') ;
         if (p)
         {
-          if (!uint160_scan(p+1, &rql.uri.port)) preexit_400(&rql, "Invalid Host header") ;
+          if (!uint160_scan(p+1, &rql.uri.port)) eexit_400(&rql, "Invalid Host header") ;
           *p = 0 ;
         }
-        if (!*x || *x == '.') preexit_400(&rql, "Invalid Host header") ;
+        if (!*x || *x == '.') eexit_400(&rql, "Invalid Host header") ;
         rql.uri.host = x ;
       }
-      else if (!rql.uri.host) preexit_400(&rql, "Missing Host header") ;
+      else if (!rql.uri.host) eexit_400(&rql, "Missing Host header") ;
     }
     else if (!rql.uri.host) rql.uri.host = g.defaulthost ;
     if (!rql.uri.port) rql.uri.port = g.defaultport ;
@@ -541,12 +544,12 @@ int main (int argc, char const *const *argv, char const *const *envp)
       {
         case TIPIDEE_TRANSFERCODING_FIXED :
         {
-          if (content_length > g.maxrqbody) exit_413(&rql, "Request body too large") ;
-          if (!stralloc_ready(&bodysa, content_length)) die500sys(&rql, 111, "stralloc_ready") ;
+          if (content_length > g.maxrqbody) exit_413(&rql, docroot) ;
+          if (!stralloc_ready(&bodysa, content_length)) die500sys(&rql, 111, docroot, "stralloc_ready") ;
           if (buffer_timed_get_g(buffer_0, bodysa.s, content_length, &deadline) < content_length)
           {
-            if (errno == ETIMEDOUT) exit_408(&rql) ;
-            else exit_400(&rql, "Request body does not match Content-Length") ;
+            if (errno == ETIMEDOUT) exit_408(&rql, docroot) ;
+            else exit_400(&rql, docroot) ;
           }
           bodysa.len = content_length ;
           break ;
@@ -555,9 +558,9 @@ int main (int argc, char const *const *argv, char const *const *envp)
         {
           if (!tipidee_util_chunked_read_g(buffer_0, &bodysa, g.maxrqbody, &deadline))
           {
-            if (error_temp(errno)) die500sys(&rql, 111, "decode chunked body") ;
-            else if (errno == EMSGSIZE) exit_413(&rql, "Request body too large") ;
-            else exit_400(&rql, "Invalid chunked body") ;
+            if (error_temp(errno)) die500sys(&rql, 111, docroot, "decode chunked body") ;
+            else if (errno == EMSGSIZE) exit_413(&rql, docroot) ;
+            else exit_400(&rql, docroot) ;
           }
           break ;
         }
@@ -569,8 +572,9 @@ int main (int argc, char const *const *argv, char const *const *envp)
 
       while (serve(&rql, docroot, uribuf, &hdr, bodysa.s, bodysa.len))
         if (localredirs++ >= MAX_LOCALREDIRS)
-          die502x(&rql, 2, "too many local redirections - possible loop involving path ", rql.uri.path) ;
+          die502x(&rql, 2, docroot, "too many local redirections - possible loop involving path ", rql.uri.path) ;
     }
   }
+
   log_and_exit(0) ;
 }