Search code examples
perlfastcgipsgi

FastCGI, Perl and Exit


I've been honing the performance a large, decades old codebase I use for projects over the last few weeks and it was suggested to me on here that I should look at something like FastCGI or HTTP::Engine. I've found it impressively straightforward to make use of FastCGI, but there's one nagging question I've found mixed answers on.

Some documents I've read say you should never call exit on a script being run through FastCGI, since that harms the whole concept of keeping it loaded persistently. Others say it doesn’t matter. My code uses exit in a lot of places where it is important to make sure nothing keeps executing. For example, I have restricted access components that call an authorization check:

use MyCode::Authorization;
our $authorization = MyCode::Authorization->new();

sub administration {
    $authorization->checkCredentials();

    #...Do restricted access stuff.
}

To make it as hard for there to be an error in the code as possible where someone would be permitted to access those functions when they shouldn't, checkCredentials ends the process with exit() after generating a user friendly response with a login page if the answer is that the user does not have the appropriate credentials. E.g.:

sub checkCredentials {
   #Logic to check credentials

   if ($validCredential) {
       return 1;
   }
   else {
       # Build web response.
       # Then:
       exit;
   }
}

}

I’ve used it so that I don’t accidentally overlook something continuing on that causes a security hole. At present, the calling routine can safely assume it only gets back control from checkCredentials if the right credentials are provided.

However, I’m wondering if I need to remove those calls to make good use of FastCGI. Is FCGI's $req->Finish() (or the equivalent in PSGI for HTTP::Engine) an adequate replacement?


Solution

  • I've read say you should never call exit on a script being run through FastCGI,

    You don't want the process to exit since the point of using FastCGI is to use a single process to handle multiple requests (to avoid load times, etc).

    So you want to do is override exit so that is ends your request-specific code, but not the FastCGI request loop.


    You can override exit, but you must do so at compile-time. So use a flag to signal whether the override is active or not.

    our $override_exit = 0;
    BEGIN { 
       *CORE::GLOBAL::exit = sub(;$) {
          die "EXIT_OVERRIDE\n" if $override_exit;
          CORE::exit($_[0] // 0);
       };
    }
    
    while (get_request()) {
       # Other setup...
    
       eval {
          local $override_exit = 1;
          handle_request();
       };
        
       my $exit_was_called = $@ eq "EXIT_OVERRIDE\n";
       log_error($@) if $@ && !$exit_was_called;
        
       log_error("Exit called\n") if $exit_was_called;
    
       # Other cleanup...
    }
    

    But that creates an exception that might be caught unintentionally. So let's use last instead.

    our $override_exit = 0;
    BEGIN { 
       *CORE::GLOBAL::exit = sub(;$) {
          no warnings qw( exiting );
          last EXIT_OVERRIDE if $override_exit;
          CORE::exit($_[0] // 0);
       };
    }
    
    while (get_request()) {
       # Other setup...
    
       my $exit_was_called = 1;
       EXIT_OVERRIDE: {
          local $override_exit = 1;
          eval { handle_request() };
          log_error($@) if $@;
          $exit_was_called = 0;
       }
    
       log_error("Exit called\n") if $exit_was_called;
    
       # Other cleanup...
    }