How to send a custom content type along with a custom response?
Recently, Steve Hay asked on the modperl user mailing list:
I have a mod_perl-2 handler that uses custom_response() to display error
messages if something goes wrong:
$r->custom_response(Apache2::Const::SERVER_ERROR, $error);
return Apache2::Const::SERVER_ERROR;
That almost works fine, but the trouble is that the Content-Type header
is always set to text/html; charset=iso-8859-1, in which the charset,
at least, is potentially wrong: all the pages in my software produce
UTF-8 and are normally output with Content-Type
application/xhtml+xml; charset=utf-8.
How do I set the Content-Type when issuing a custom_reponse()? The usual
thing of calling
$r->content_type("application/xhtml+xml; charset=utf-8");
doesn't seem to work in this case.
Why doesn't that work?
The culprit is the function ap_send_error_response() in httpd-2.2.x/modules/http/http_protocol.c.
It contains this piece of code:
if (!r->assbackwards) {
[...]
if (apr_table_get(r->subprocess_env,
"suppress-error-charset") != NULL) {
core_request_config *request_conf =
ap_get_module_config(r->request_config, &core_module);
request_conf->suppress_charset = 1; /* avoid adding default
* charset later
*/
ap_set_content_type(r, "text/html");
}
else {
ap_set_content_type(r, "text/html; charset=iso-8859-1");
}
[...]
}
You see, there is no way to get around that text/html or text/html; charset=iso-8859-1 unless
the assbackwards flag is set.
One solution
That leads to a solution. Let's try to set that flag. In Perl that would look like $r->assbackwards(1).
What does this flag mean? Well, it tells apache the content generator (your module) knows better how to generate
HTTP header fields.
Here is a simple PerlResponseHandler:
sub handler {
my ($r)=@_;
$r->assbackwards(1);
$r->custom_response( 403, "sorry, no access" );
return 403;
}
Let's see what happens. I use netcat to really see what is written to the browser:
$ echo -en 'GET /test HTTP/1.0\r\nHost: localhost\r\n\r\n' | netcat localhost 80
sorry, no access
Oops, where is the HTTP status line, where the header fields? You'd have expected something like this, me thinks:
$ echo -en 'GET /test HTTP/1.0\r\nHost: localhost\r\n\r\n' | netcat localhost 80
HTTP/1.1 403 Forbidden
Date: Sun, 14 Mar 2010 13:36:49 GMT
Server: Apache
Content-Length: 17
Connection: close
Content-Type: text/html; charset=iso-8859-1
sorry, no access
This is the effect of assbackwards. You have to create the HTTP protocol stuff yourself:
sub handler {
my ($r)=@_;
$r->assbackwards(1);
my $header=<<'HEADER';
HTTP/1.0 403 Huhu Haha
Date: Sun, 14 Mar 2010 13:36:49 GMT
Content-Type: text/plain; charset=my-characters
HEADER
my $msg="sorry, no access\n";
$msg=$header.'Content-Length: '.length($msg)."\n\n".$msg;
$r->custom_response( 403, $msg );
return 403;
}
$ echo -en 'GET /test HTTP/1.0\r\nHost: localhost\r\n\r\n' | netcat localhost 80
HTTP/1.0 403 Huhu Haha
Date: Sun, 14 Mar 2010 13:36:49 GMT
Content-Type: text/plain; charset=my-characters
Content-Length: 17
sorry, no access
This should be understood by a browser. But isn't there a better solution. One that has apache handle the HTTP protocol stuff?
A better way
Perhaps you know or have at least read that the ErrorDocument directive also accepts and URI.
ModPerl's $r->custom_response() does just the same. ap_die(), apache's function
to handle all errors, will then generate an internal redirect to that URI instead of sending it as response text.
So, the general idea is to establish a modperl handler as custom response that then can set header fields as it likes and have it send the error text. How to do that?
At first, we need some place to store the error message so that the custom response handler can access it. Since
the handler is normally run by the same Perl interpreter a global variable will do. But you have to watch out for
subrequests and similar stuff that can reset the global variable. So, this is not a good solution.
But there is $r->pnotes or even $r->notes to the rescue.
So, instead of passing the error text directly to custom_response we store it in pnotes and
set an otherwise unused URI, say /-/error, as custom_response:
sub handler {
my ($r)=@_;
@{$r->pnotes}{qw/etext ect/}=("sorry, no access\n", 'text/plain; charset=my-characters');
$r->custom_response( 403, "/-/error" );
return 403;
}
Now, we need to configure /-/error to run a Perl handler:
<Location /-/error>
SetHandler modperl
PerlResponseHandler My::Error
</Location>
And, of course, we need the handler function, My::Error::handler:
sub handler {
my ($r)=@_;
return Apache2::Const::NOT_FOUND unless $r->prev;
$r->content_type($r->prev->pnotes->{ect});
$r->print($r->prev->pnotes->{etext});
return Apache2::Const::OK;
}
So, let's have a look at the outcome:
$ echo -en 'GET /test HTTP/1.0\r\nHost: localhost\r\n\r\n' | netcat localhost 80
HTTP/1.1 403 Forbidden
Date: Sun, 14 Mar 2010 16:39:56 GMT
Server: Apache
Connection: close
Content-Type: text/plain; charset=my-characters
sorry, no access
Note the custom content type header. So, that's probably what Steve was looking for.
Letzte Aktualisierung: 14.03.2010

