home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Writing Apache Modules with Perl and C
By:   Lincoln Stein and Doug MacEachern
Published:   O'Reilly & Associates, Inc.  - March 1999

Copyright © 1999 by O'Reilly & Associates, Inc.


 


   Show Contents   Previous Page   Next Page

Chapter 4 - Content Handlers / Handling Errors
HTTP Headers and Error Handling

You already know about using header_out() to set HTTP header fields. A properly formatted HTTP header is sent to the browser when your module explicitly calls send_http_header(), or it is sent for you automatically if you are using Apache::Registry, the PerlSendHeader directive is set to On, and your script prints some text that looks like an HTTP header.

You have to be careful, however, if your module ever returns non-OK status codes. Apache wants to assume control over the header generation process in the case of errors; if your module has already sent the header, then Apache will send a redundant set of headers with unattractive results. This applies both to real HTTP errors, like BAD_REQUEST and NOT_FOUND, as well as to nonfatal conditions like REDIRECT and AUTH_REQUIRED.

Consider the following fishy example:

package Apache::Crash;
# File: Apache/Crash.pm
use strict;
use Apache::Constants qw(:common);
use constant CRASH => 1;
sub handler {
   my $r = shift;
   $r->content_type('text/plain');
   $r->send_http_header;
   return OK if $r->header_only;
   return SERVER_ERROR if CRASH;
   $r->print('Half a haddock is better than none.');
   return OK;
}
1;
__END__

After setting the document MIME type, this module sends off the HTTP header. It then checks a constant named CRASH and if true, which it always is, returns a status code of SERVER_ERROR. Apache would ordinarily send a custom HTTP header in response to this status code, but because the module has already emitted a header, it's too late. Confusion results. If we map this module to the URI /Crash, we can telnet directly to the server to demonstrate the problem:

% telnet www.modperl.com 80
Trying 192.168.2.5...
Connected to modperl.com.
Escape character is '^]'.
GET /Crash HTTP/1.0
HTTP/1.1 200 OK
Date: Thu, 21 May 1998 11:31:40 GMT
Server: Apache/1.3b6
Connection: close
Content-Type: text/plain
HTTP/1.1 200 OK
Date: Thu, 21 May 1998 11:31:40 GMT
Server: Apache/1.3b6
Connection: close
Content-Type: text/html
<HTML><HEAD>
<TITLE>500 Internal Server Error</TITLE>
</HEAD><BODY>
<H1>Internal Server Error</H1>
The server encountered an internal error or
misconfiguration and was unable to complete
your request.<P>
</BODY></HTML>
Connection closed by foreign host.

Not only are there two HTTP headers here, but both of them indicate a status code of 200 OK, which is definitely not right. When displayed in the browser, the page will be marred by extraneous header lines at the top of the screen.

The cardinal rule is that you should never call Apache::send_http_header() until your module has completed all its error checking and has decided to return an OK status code. Here's a better version of Apache::Crash that avoids the problem:

package Apache::Crash;
# File: Apache/Crash.pm
use strict;
use Apache::Constants qw(:common);
use constant CRASH => 1;
sub handler {
   my $r = shift;
   return SERVER_ERROR if CRASH;
   $r->content_type('text/plain');
   $r->send_http_header;
   return OK if $r->header_only;
   $r->print('Half a haddock is better than none.');
   return OK;
}
1;
__END__

Now when we telnet to the server, the server response looks the way it should:

(~) 103% telnet www.modperl.com 80
Trying 192.168.2.5...
Connected to modperl.com.
Escape character is '^]'.
GET /Crash HTTP/1.0
HTTP/1.1 500 Internal Server Error
Date: Thu, 21 May 1998 11:40:56 GMT
Server: Apache/1.3b6
Connection: close
Content-Type: text/html
<HTML><HEAD>
<TITLE>500 Internal Server Error</TITLE>
</HEAD><BODY>
<H1>Internal Server Error</H1>
The server encountered an internal error or
misconfiguration and was unable to complete
your request.<P>
</BODY></HTML>

Another important detail about error handling is that Apache ignores the fields that you set with header_out() when your module generates an error status or invokes an internal redirect. This is usually not a problem, but there are some cases in which this restriction can be problematic. The most typical case is the one in which you want a module to give the browser a cookie and immediately redirect to a different URI. Or you might want to assign an error document to the UNAUTHORIZED status code so that a custom login screen appears when the user tries to access a restricted page. In both cases you need to manipulate the HTTP header fields prior to the redirect.

For these cases, call the request object's err_header_out() method. It has identical syntax to header_out(), but the fields that you set with it are sent to the browser only when an error has occurred. Unlike ordinary headers, the fields set with err_header_out() persist across internal redirections, and so they are passed to Apache ErrorDocument handlers and other local URIs.

This provides you with a simple way to pass information between modules across internal redirects. Combining the example from this section with the example from the previous section gives the modules shown in Example 4-18. Apache::GoFish generates a SERVER_ERROR, which is intercepted and handled by the custom ErrorDocument handler named Apache::Carp (Example 4-19). Before relinquishing control, however, Apache::GoFish creates a custom HTTP field named X-Odor which gives the error handler something substantial to complain about. The end result is shown in Figure 4-10.

Figure 4-10. When Apache::GoFish generates a custom error document, it displays the contents of the custom X-Odor header.

The code should be fairly self-explanatory. The main point to notice is Apache::GoFish's use of err_header_out() to set the value of the X-Odor field, and Apache::Carp's use of the same function to retrieve it. Like header_out(), when you call err_header_out() with a single argument, it returns the current value of the field and does not otherwise alter the header. When you call it with two arguments, it sets the indicated field.

An interesting side effect of this technique is that the X-Odor field is also returned to the browser in the HTTP header. This could be construed as a feature. If you wished to pass information between the content handler and the error handler without leaving tracks in the HTTP header, you could instead use the request object's "notes" table to pass messages from one module to another. Chapter 9 covers how to use this facility (see the description of the notes() method under "Server Core Functions").

Example 4-18. Invoking a Custom Error Handler Document

package Apache::GoFish;
# File: Apache/GoFish.pm
use Apache::Constants qw(:common :response);
use constant CRASH=>1;
sub handler {
  my $r = shift;
  $r->err_header_out('X-Odor'=>"something's rotten in Denmark");
  $r->custom_response(SERVER_ERROR, "/Carp");
  return SERVER_ERROR if CRASH;
  $r->content_type('text/plain');
  $r->send_http_header;
  return OK if $r->header_only;
  $r->print('Half a haddock is better than none.');
  return OK;
}
1;
__END__

Here is a sample configuration entry:

<Location /GoFish>
  SetHandler perl-script
  PerlHandler Apache::GoFish
</Location> 

Example 4-19. An Error Handler to Complement the Previous Example

package Apache::Carp;
# File: Apache/Carp.pm
use strict;
use Apache::Constants qw(:common);
use CGI qw(:html);
sub handler {
   my $r = shift;
   my $odor = $r->err_header_out('X-Odor');
   $odor ||= 'unspecified odor';
   $r->content_type('text/html');
   $r->send_http_header;
   return OK if $r->header_only;
    my $original_request = $r->prev;
   my $original_uri = $original_request ? $original_request->uri : '';
   my $admin = $r->server->server_admin;
    $r->print(
            start_html(-title => 'Phew!!', -bgcolor => 'white'),
            h1('Phew!!'),
            p("Something fishy happened while processing this request."),
            p("The odor was ", strong($odor), '.'),
            hr,
            address(a({-href => "mailto:$admin"}, 'webmaster')),
            end_html
            );
    return OK;
}
1;
__END__

Here is a sample configuration entry:

<Location /Carp>
  SetHandler  perl-script 
  PerlHandler Apache::Carp
</Location>
   Show Contents   Previous Page   Next Page
Copyright © 1999 by O'Reilly & Associates, Inc.