2

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?

Timothy R. Butler
  • 1,097
  • 7
  • 20
  • Same answer as [How to override exit() call in Perl eval block](https://stackoverflow.com/q/25375579/589924) – ikegami Apr 12 '21 at 00:25

1 Answers1

2

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...
}
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • This is really a cool bit of code, although I'm still sort of uncertain regarding the alternative, which is where the linked to "duplicate" question Stack Overflow notes doesn't quite hit my point of confusion. If I override `exit`, how do I ensure the same equivalent? E.g. that processing doesn't continue, but in the case of a FastCGI process, that I immediately return back to my blocking `while` loop to wait for another request. – Timothy R. Butler Apr 12 '21 at 00:18
  • I reopened it, but there's no point in using alternatives since you should already be using this. – ikegami Apr 12 '21 at 00:20
  • Equilvalent of what? If you're asking "*If I override exit, how do I ensure I immediately return back to my blocking while loop to wait for another request.equivalent?*", that's exactly what the answer shows you how to do. – ikegami Apr 12 '21 at 00:21
  • Sorry, I wasn't clear on that. So the handle_request() part is ensuring I'm back to my while loop? – Timothy R. Butler Apr 12 '21 at 00:23
  • No, `last EXIT_OVERRIDE` does. See update. – ikegami Apr 12 '21 at 00:24
  • Thank you so much for explaining this. Please forgive me if I'm being dense, I just want to make sure I'm getting this correct so I don't introduce a security issue. The app logic would go in the `handle_request()` subroutine, correct? And when exit is called, that ends any further execution of code within the code labeled `EXIT_OVERRIDE`, correct? – Timothy R. Butler Apr 12 '21 at 01:12
  • Try it and see :) – ikegami Apr 12 '21 at 01:17
  • Wow, I never thought "exiting" would make me this excited! Thank you! – Timothy R. Butler Apr 12 '21 at 01:55
  • 1
    Oh, I guess I should mention that `{ ... }` (as a statement, not the hash contructor) is technically a loop. Called "bare loop" in the docs (IIRC), it's a loop that executes exactly once. So `last` simply exits that loop. (The label handles nesting.) – ikegami Apr 12 '21 at 03:25
  • Oh! That makes perfect sense now regarding how that last affects EXIT_OVERRIDE. Thank you! – Timothy R. Butler Apr 12 '21 at 03:28