PHP 8.4: New request_parse_body function

TypeNew Feature

PHP automatically parses HTTP POST requests to populate the $_POST and $_FILES super global variables. However, other HTTP requests with methods such as PUT and PATCH do not get parsed automatically, and it is up to the PHP application to parse the request data.

With the popularity of the REST APIs that increasingly utilize HTTP methods such as PUT, DELETE, and PATCH, parsing HTTP request data consistently is important. However, starting to automatically parse HTTP request data for non-POST requests can be a breaking change for existing PHP applications.

PHP provides a stream wrapper at php://input, that contains the request data. For POST requests with enctype="multipart/form-data", this stream wrapper remains empty because it's automatically parsed and consumed to populate $_POST and $_FILES variables. The automatic $_POST/$_FILES processing can be controlled with the enable_post_data_reading=Off INI setting.

When the enable_post_data_reading INI value is set to Off, or when the HTTP request method is a value apart from POST, the php://input stream wrapper can be read in user-land PHP code to parse the HTTP request data.

curl --request PUT \
     --location '' \
     --form 'test="123"'

From a PHP application, the form data of the Curl call above can be read from the php://input stream.

echo file_get_contents('php://input');
Content-Disposition: form-data; name="test"

New request_parse_body function

PHP 8.4 adds a new function named request_parse_body that exposes the PHP's built-in request parsing functionality for other HTTP request methods.

 * Parse and consume php://input and return the values for $_POST
 * and $_FILES variables.
 * @param array<string, int|string>|null $options Overrides for INI values
 * @return array<int, array> Array with key 0 being the post data  
 *   (similar to $_POST), and key 1 being the files ($_FILES).  
function request_parse_body(?array $options = null): array {}

When called, the request_parse_body function reads the entire contents that are otherwise available on the php://input stream and creates the values that can be used in $_POST and $_FILES variables.

The return value will be an array containing two keys, 0 and 1, containing the parsed values that can be used as $_POST (array key index 0) and $_FILES (index 1). Both array keys will be present at all times — even if there are no request data, and/or files.

It is possible to populate the $_POST and $_FILES values directly from the return values:

[$_POST, $_FILES] = request_parse_body();

Note that the request parsing continues to be bound to the limitations set by the INI directives. For example, if the post_max_size directive (which limits the max size of the requests) is set to 2000 bytes, attempting to call the request_parse_body function with a request larger than that continues to result in an error.

The request parsing options can be overridden with smaller or larger values by passing the $options parameter.

If the request being attempted to parse violates the limits set at the INI directives or the custom options, the request_parse_body function throws a new exception named RequestParseBodyException.

Overriding request parsing options

The $options parameter can be used to pass an array of INI values related to request parsing. These values do not need to be smaller than the global configuration. This gives the advantage to selectively process smaller or larger limits than the limits set at INI files.

For example, to parse a request with a higher or smaller limit to the post_max_size INI directive, call the request_parse_body function with the desired new value:

request_parse_body(['post_max_size' => 1024]);

The $options array only accepts the following overrides:

INI/$option key Description
post_max_size Maximum size of POST data that PHP will accept. Its value may be 0 to disable the limit.
max_input_vars How many GET/POST/COOKIE input variables may be accepted
max_multipart_body_parts How many multipart body parts (combined input variable and file uploads) may be accepted.
max_file_uploads Maximum number of files that can be uploaded via a single request
upload_max_filesize Maximum allowed size for uploaded files.

The values for these keys must be an integer or a quantity string (such as the values allowed by the ini_parse_quantity function.

Passing INI directives apart from the list above lists in a ValueError exception:

request_parse_body(['arbitrary_value' => 42]);
ValueError: Invalid key 'arbitrary_value' in $options argument

Passing non-integer and non-quantity string values results in a PHP warning:

request_parse_body(['post_max_size' => 'arbitrary_value']);
Warning: Invalid quantity "arbitrary_value": no valid leading digits, interpreting as "0" for backwards compatibility

Passing non-string and non-integer values as values to the $options keys results in a ValueError exception:

request_parse_body(['post_max_size' => []]);
ValueError: Invalid array value in $options argument

RequestParseBodyException Exception Class

RequestParseBodyException is a new Exception class declared in the global namespace, extending the Exception class.

class RequestParseBodyException extends Exception {}

RequestParseBodyException exceptions are thrown if the request_parse_body function can not parse the request data. This can occur if the provided request data are invalid, does not send a Content-Type header, or if the request data falls outside the constraint set by INI directives and the optional $options parameter.

The following is a list of RequestParseBodyException exception cases and their cause:

  • RequestParseBodyException: Request does not provide a content type
    The request does not contain a Content-Type header.

  • RequestParseBodyException: Content-Type ARBITRARY_TYPE is not supported
    The Content-Type header contains a value other than multipart/form-data or application/x-www-form-urlencoded.

  • RequestParseBodyException: Missing boundary in multipart/form-data POST data
    The request does not contain a boundary. Make sure the request is correctly formatted as multipart/form-data or application/x-www-form-urlencoded.

  • RequestParseBodyException: POST Content-Length of ... bytes exceeds the limit of ... bytes
    The content length has exceeded the post_max_size value set at the ($options parameter](#options) or INI directive.

  • RequestParseBodyException: Multipart body parts limit exceeded ... To increase the limit change max_multipart_body_parts in php.ini
    The request data parts have exceeded the max_multipart_body_parts value set at the ($options parameter](#options) or the INI directive.

  • RequestParseBodyException: Input variables exceeded ... To increase the limit change max_input_vars in php.ini.
    The request data parts have exceeded the max_input_vars value set at the ($options parameter](#options) or the INI directive.

  • RequestParseBodyException: Maximum number of allowable file uploads has been exceeded
    The number of files being uploaded exceeds the max_file_uploads value set at the ($options parameter](#options) or the INI directive.

request_parse_body caveats

request_parse_body function is designed to be called only one time per request. It provides no way to specify the string to be parsed, and it destructively consumes the php://input. On subsequent calls, the function will return an array with empty data, and the php://input stream will be empty after the first request_parse_body() call.

Non-idempotent behavior of request_parse_body
Note that calling the request_parse_body function has potential destructive behavior, including it consuming the php://input stream and emptying its content.

  • Calling request_parse_body function consumes the php://input stream. The php://input stream will be empty
  • If the php://input stream was read previously (e.g. file_get_contents('php://input'), request_parse_body function returns an empty result (i.e. [0 => [], 1 => []]).
  • request_parse_body function does not directly modify $_POST and $_FILES global variables; it is up to the PHP application to overwrite those variables if desired.
  • Only the first call of request_parse_body function returns the parsed data. Subsequent calls return an empty result (i.e. [0 => [], 1 => []]).
  • Even if the function throws an exception, the php://input is still consumed and cleared, and subsequent request_parse_body calls return an empty result.

Backward Compatibility Impact

This function cannot be polyfilled because it needs to utilize calls to the underlying Server API (SAPI) to obtain the raw data.

This change should not cause any backward compatibility issues unless a PHP application declares its own request_parse_body function.

RFC Discussion Implementation