96

I'm working on a PHP form that attaches a file to an email, and trying to gracefully handle cases where the uploaded file is too large.

I've learned that there are two settings in php.ini that affect the maxiumum size of a file upload: upload_max_filesize and post_max_size.

If a file's size exceeds upload_max_filesize, PHP returns the file's size as 0. That's fine; I can check for that.

But if it exceeds post_max_size, my script fails silently and goes back to the blank form.

Is there any way to catch this error?

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 1
    Do you have access to php.ini? post_max_size should be set larger than upload_max_filesize. You should also be using on the form as outlined http://ca2.php.net/manual/en/features.file-upload.post-method.php – Matt McCormick Jan 25 '10 at 16:43
  • @Matt McCormick - the MAX_FILE_SIZE input works great - if the file size exceeds that, the file size now shows as 0, which is a case I've already got handled. Even though this can be bypassed by a malicious user, it serves my purposes here, because I'm just trying to fail gracefully for regular users. – Nathan Long Jan 25 '10 at 16:53

7 Answers7

59

From the documentation :

If the size of post data is greater than post_max_size, the $_POST and $_FILES superglobals are empty. This can be tracked in various ways, e.g. by passing the $_GET variable to the script processing the data, i.e. <form action="edit.php?processed=1">, and then checking if $_GET['processed'] is set.

So unfortunately, it doesn't look like PHP sends an error. And since it sends am empty $_POST array, that is why your script is going back to the blank form - it doesn't think it is a POST. (Quite a poor design decision IMHO)

This commenter also has an interesting idea.

It seems that a more elegant way is comparison between post_max_size and $_SERVER['CONTENT_LENGTH']. Please note that the latter includes not only size of uploaded file plus post data but also multipart sequences.

Jonathan Bergeron
  • 1,864
  • 2
  • 13
  • 15
Matt McCormick
  • 13,041
  • 22
  • 75
  • 83
  • 4
    Please read the bolded part. That is if the file size exceeds upload_max_filesize, the user is asking what happens when the form exceeds post_max_size. post_max_size should be set higher than upload_max_filesize to try to avoid this issue but the OP may have reasons for keeping it the same. – Matt McCormick Jan 25 '10 at 16:29
  • 1
    Sorry, I mixed up post_max_size and upload_max_filesize. +1 – Pekka Jan 25 '10 at 16:40
  • @Matt - post_max_size IS set higher than upload_max_filesize, but I still get the failure if the upload exceeds both. If it falls between the two, I see that the file size shows 0. – Nathan Long Jan 25 '10 at 16:45
  • Is post_max_size available to a PHP script for comparision purposes? – Nathan Long Jan 25 '10 at 16:47
  • 1
    You've solved it, but yes post_max_size is available. Just do ini_get('post_max_size'). ini_get() can also be used to check other INI settings. – Matt McCormick Jan 25 '10 at 17:01
  • @Matt - I will make a note of ini_get(). Thank you for your help. You rock. – Nathan Long Jan 25 '10 at 17:34
  • So far, it looks like $_SERVER['CONTENT_LENGTH'] is not available - if I exceed `max_post_size`, my script terminates. I can't even echo that value. Oh well. – Nathan Long Jan 25 '10 at 20:40
  • @NathanLong $_SERVER['CONTENT_LENGTH'] is only set when the request method is POST. Have you checked that? – Savas Vedova Apr 02 '13 at 13:48
  • 1
    @SavasVedova - thanks for pointing that out, but I can no longer troubleshoot. It's been a few years and I no longer work at that company or in PHP. :) – Nathan Long Apr 02 '13 at 14:33
47

there is a way to catch / handle files exceeding max post size, this is my preferred on, as it tells the end user what has happened and who is at fault ;)

if (empty($_FILES) && empty($_POST) &&
        isset($_SERVER['REQUEST_METHOD']) &&
        strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
    //catch file overload error...
    $postMax = ini_get('post_max_size'); //grab the size limits...
    echo "<p style=\"color: #F00;\">\nPlease note files larger than {$postMax} will result in this error!<br>Please be advised this is not a limitation in the CMS, This is a limitation of the hosting server.<br>For various reasons they limit the max size of uploaded files, if you have access to the php ini file you can fix this by changing the post_max_size setting.<br> If you can't then please ask your host to increase the size limits, or use the FTP uploaded form</p>"; // echo out error and solutions...
    addForm(); //bounce back to the just filled out form.
}
else {
    // continue on with processing of the page...
}
ingomueller.net
  • 4,097
  • 2
  • 36
  • 33
AbdullahAJM
  • 471
  • 4
  • 2
  • 2
    This works when track_errors = Off, possibly also display_errors = Off and display_startup_errors = Off in php.ini. Otherwise PHP will not even get that far and send the warning, like in the question title. But in a production system this should be the php.ini setting, so this works great. – raoulsson Dec 15 '13 at 20:43
  • 2
    This neat idea is working fine in my tests (PHP/5.5.8). It can also be enhanced taking `$_SERVER['CONTENT_LENGTH']` and `upload_max_filesize` into account. – Álvaro González Jan 20 '14 at 11:05
  • Leading on from [raoulsson](http://stackoverflow.com/questions/2133652/how-to-gracefully-handle-files-that-exceed-phps-post-max-size#comment30822274_6805930)'s comment, is there any way to suppress the warning if you're not in an environment with errors suppressed? – brendo Oct 10 '14 at 10:49
  • This detection has some false positives. A max input time exceeded or a too long pause in chunked transfers will also present with empty POST and FILES superglobals. – Gerrit Oct 10 '21 at 14:19
7

We got the problem for SOAP requests where a check for emptiness of $_POST and $_FILES doesn't work, because they are also empty on valid requests.

Therefore we implemented a check, comparing CONTENT_LENGTH and post_max_size. The thrown Exception is later on transformed into a XML-SOAP-FAULT by our registered exception handler.

private function checkPostSizeExceeded() {
    $maxPostSize = $this->iniGetBytes('post_max_size');

    if ($_SERVER['CONTENT_LENGTH'] > $maxPostSize) {
        throw new Exception(
            sprintf('Max post size exceeded! Got %s bytes, but limit is %s bytes.',
                $_SERVER['CONTENT_LENGTH'],
                $maxPostSize
            )
        );
    }
}

private function iniGetBytes($val)
{
    $val = trim(ini_get($val));
    if ($val != '') {
        $last = strtolower(
            $val{strlen($val) - 1}
        );
    } else {
        $last = '';
    }
    switch ($last) {
        // The 'G' modifier is available since PHP 5.1.0
        case 'g':
            $val *= 1024;
            // fall through
        case 'm':
            $val *= 1024;
            // fall through
        case 'k':
            $val *= 1024;
            // fall through
    }

    return $val;
}
staabm
  • 1,535
  • 22
  • 20
  • @manuel-azar: the added "break" stements are not correct. those dont belong there.. see https://3v4l.org/ABfGs – staabm Feb 23 '17 at 09:01
  • It's unnecessary to check the check if the size exceeds. There would be no content_length if there was no file. So you just have to check to see if content_length is set and the post and file variables are empty. No? – ADJenks Mar 26 '19 at 22:39
  • Chunked transfers have no Content-Length header so the check on that header won't help you in those cases. – Gerrit Oct 10 '21 at 14:15
4

Building on @Matt McCormick's and @AbdullahAJM's answers, here is a PHP test case that checks the variables used in the test are set and then checks if the $_SERVER['CONTENT_LENGTH'] exceeds the php_max_filesize setting:

            if (
                isset( $_SERVER['REQUEST_METHOD'] )      &&
                ($_SERVER['REQUEST_METHOD'] === 'POST' ) &&
                isset( $_SERVER['CONTENT_LENGTH'] )      &&
                ( empty( $_POST ) )
            ) {
                $max_post_size = ini_get('post_max_size');
                $content_length = $_SERVER['CONTENT_LENGTH'] / 1024 / 1024;
                if ($content_length > $max_post_size ) {
                    print "<div class='updated fade'>" .
                        sprintf(
                            __('It appears you tried to upload %d MiB of data but the PHP post_max_size is %d MiB.', 'csa-slplus'),
                            $content_length,
                            $max_post_size
                        ) .
                        '<br/>' .
                        __( 'Try increasing the post_max_size setting in your php.ini file.' , 'csa-slplus' ) .
                        '</div>';
                }
            }
Lance Cleveland
  • 3,098
  • 1
  • 33
  • 36
  • This is the most logical way to do it. Just check if _post is empty but the content length is not. No need to compare sizes. – ADJenks Mar 26 '19 at 22:40
2

That is a simple way to fix this problem:

Just call "checkPostSizeExceeded" on begin of your code

function checkPostSizeExceeded() {
        if (isset($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] == 'POST' and
            isset($_SERVER['CONTENT_LENGTH']) and empty($_POST)//if is a post request and $_POST variable is empty(a symptom of "post max size error")
        ) {
            $max = get_ini_bytes('post_max_size');//get the limit of post size 
            $send = $_SERVER['CONTENT_LENGTH'];//get the sent post size

            if($max < $_SERVER['CONTENT_LENGTH'])//compare
                throw new Exception(
                    'Max size exceeded! Were sent ' . 
                        number_format($send/(1024*1024), 2) . 'MB, but ' . number_format($max/(1024*1024), 2) . 'MB is the application limit.'
                    );
        }
    }

Remember copy this auxiliar function:

function get_ini_bytes($attr){
    $attr_value = trim(ini_get($attr));

    if ($attr_value != '') {
        $type_byte = strtolower(
            $attr_value{strlen($attr_value) - 1}
        );
    } else
        return $attr_value;

    switch ($type_byte) {
        case 'g': $attr_value *= 1024*1024*1024; break;
        case 'm': $attr_value *= 1024*1024; break;
        case 'k': $attr_value *= 1024; break;
    }

    return $attr_value;
}
Doglas
  • 642
  • 1
  • 11
  • 22
1

I had the same problem, and combined some of the solutions already posted here on this page (by @Doblas, @Lance Cleveland and @AbdullahAJM).

Additionally, my solution tries to sends a 413 Payload Too Large error (instead of 200 OK), which is of course only possible, when php.ini is not configured to display warnings.

// Check for Warning: php catch Warning: Unknown: POST Content-Length of bytes exceeds the limit of bytes in Unknown on line 0
// Sending 413 only works, if Warnings are turned off in php.ini!!!

// grab the size limits...
$postMaxSize = trim(ini_get('post_max_size')); 
if (strlen($postMaxSize)>0) {
   $postMaxSizeValue = substr($postMaxSize, 0, -1);
   $postMaxSizeUnit = strtolower(substr($postMaxSize, -1));
   $postMaxSize = 0; // make it fail save
   if (false !== filter_var($postMaxSizeValue, FILTER_VALIDATE_INT, array('options' => array( 'min_range' => 0)))) {
      switch ($postMaxSizeUnit) {
         case 'g': $postMaxSizeValue*=1024; // ... and fall through
         case 'm': $postMaxSizeValue*=1024; // ... and fall through
         case 'k': $postMaxSizeValue*=1024; break;
         default: if ($postMaxSizeUnit>='0' && $postMaxSizeUnit<='9') {
                     $postMaxSizeValue = (int) $postMaxSizeValue.$postMaxSizeUnit;
                  } else {
                     $postMaxSizeValue = 0;
                  }
      }
      $postMaxSize = $postMaxSizeValue;
   }
} else {
   $postMaxSize = 0;
}

if (empty($_FILES) && empty($_POST) &&
    isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'POST' &&
    isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] > $postMaxSize) {
    // if is a post request and $_POST variable is empty(a symptom of "post max size error")
    
    if (headers_sent()) {
       // echo out error and solutions...
       echo "<p style=\"color: #F00;\">\nPlease note that an error <b>413 Payload Too Large</b> should be sent, but the warning can't be catched, and so the client gets a <b>200 OK</b>. ".
            "Please turn off warnings in php.ini in order to achieve the correct behaviour.</p>"; 
    } else {
       http_response_code(413);
    }

    // catch file overload error: echo out error and solutions...
    echo "<p style=\"color: #F00;\">\nPlease note files larger than ".$postMaxSize." will result in this error!<br>".
         "Please be advised this is not a limitation in the script, this is a limitation of the hosting server.</p>";
    exit(1);
}
BogisW
  • 401
  • 2
  • 16
0

My 2 cents. Same idea, much lighter than other answers. Just call checkPostSize();. If exceeding, it will die with correct HTTP code, and log the error.

function checkPostSize() {
  preg_match('~(\d+)([KMG])*~', ini_get('post_max_size'), $matches);
  list(, $number, $unit) = $matches;
  $postMaxSize = $number * ($unit ? pow(1024, ['K' => 1, 'M' => 2, 'G' => 3][$unit]) : 1);

  if ((int)$_SERVER['CONTENT_LENGTH'] > $postMaxSize) {
    error_log('post_max_size of ' . ($postMaxSize / pow(1024, 2)) . 'M exceeded: ' . ~~($_SERVER['CONTENT_LENGTH'] / pow(1024, 2)) . 'M received.');
    http_response_code(413);
    exit;
  }
}

Also note that when your payload is big enough, Chrome will evict the response from the inspector and you can't see anything (it doesn't mean your PHP is broken). So it's a good idea to log errors for this purpose.

Chrome inspector

And I don't think checking that the method is POST and $_FILES & $_POST are empty, is enough to deduct it's a post max size exceeded case, like another answer suggests. :man_shrugging:

antoni
  • 5,001
  • 1
  • 35
  • 44