Torsten Förtsch
IT System Development & Security
Kaum macht man's richtig, schon geht's, ;-)

>> Home >> ModPerl >> custom_response content type


Content

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