Commit b8c43c35d11 for php.net
commit b8c43c35d11a1d71851d19db3514b6d902ce9903
Author: Sjoerd Langkemper <sjoerd-github@linuxonly.nl>
Date: Tue Aug 25 19:51:29 2026 +0100
sapi/cli: support Expect 100-continue in PHP dev server
When posting large payloads, curl checks whether the server is ready for
the body. It sends an `Expect: 100-continue` header and expects
`HTTP/1.1 100 Continue` as the response before sending the body. The PHP
development server did not support this, causing a timeout in curl. This
made such requests take one second longer.
HTTP/1.0 does not support this, so the response is only sent when the
request is HTTP/1.1.
- https://everything.curl.dev/http/post/expect100.html
- https://github.com/php/php-src/issues/23242
Fix GH-23242
Close GH-23245
diff --git a/NEWS b/NEWS
index 0817b78f659..5276069926e 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,10 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.6.0beta3
+- CLI:
+ . Fixed bug GH-23242 (PHP development server does not support Expect
+ 100-continue flow control). (Sjoerd Langkemper)
+
27 Aug 2026, PHP 8.6.0beta2
diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c
index 5df12648ca8..9f13f064770 100644
--- a/sapi/cli/php_cli_server.c
+++ b/sapi/cli/php_cli_server.c
@@ -176,6 +176,7 @@ typedef struct php_cli_server_client {
bool request_read;
bool too_large_post;
bool headers_written;
+ bool expect_continue;
zend_string *current_header_name;
zend_string *current_header_value;
enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element;
@@ -1794,6 +1795,13 @@ static int php_cli_server_client_read_request_on_headers_complete(php_http_parse
return 2;
}
+ zval *expect_val = zend_hash_str_find(&client->request.headers, "expect", sizeof("expect") - 1);
+ if (expect_val && Z_TYPE_P(expect_val) == IS_STRING
+ && zend_string_equals_literal_ci(Z_STR_P(expect_val), "100-continue")
+ && parser->http_major == 1 && parser->http_minor == 1) {
+ client->expect_continue = true;
+ }
+
return 0;
}
@@ -1901,6 +1909,23 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha
return -1;
}
+ if (client->expect_continue && !client->request_read) {
+ /* Parser completed headers with Expect: 100-continue but hasn't
+ * finished reading the body. Send 100 Continue before the client
+ * sends the request body. Only supported in HTTP/1.1. */
+ static const char continue_response[] = "HTTP/1.1 100 Continue\r\n\r\n";
+ bool send_success = false;
+ client->expect_continue = false;
+ zend_try {
+ size_t sent = php_cli_server_client_send_through(client, continue_response, strlen(continue_response));
+ send_success = sent == strlen(continue_response);
+ } zend_end_try();
+ if (!send_success) {
+ *errstr = php_socket_strerror(php_socket_errno(), NULL, 0);
+ return -1;
+ }
+ }
+
return client->request_read ? 1: 0;
}
/* }}} */
@@ -1985,6 +2010,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se
client->request_read = false;
client->too_large_post = false;
client->headers_written = false;
+ client->expect_continue = false;
client->last_header_element = HEADER_NONE;
client->current_header_name = NULL;
diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc
index feee2bbb568..ec370753573 100644
--- a/sapi/cli/tests/php_cli_server.inc
+++ b/sapi/cli/tests/php_cli_server.inc
@@ -5,6 +5,7 @@ class CliServerInfo {
public function __construct(
public string $docRoot,
public $processHandle,
+ public $outputFile,
) {}
}
@@ -118,7 +119,7 @@ function php_cli_server_start(
define("PHP_CLI_SERVER_PORT", $port);
define("PHP_CLI_SERVER_ADDRESS", PHP_CLI_SERVER_HOSTNAME.":".PHP_CLI_SERVER_PORT);
- return new CliServerInfo($doc_root, $handle);
+ return new CliServerInfo($doc_root, $handle, $output_file);
}
function php_cli_server_connect() {
diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt
new file mode 100644
index 00000000000..37886fc25e1
--- /dev/null
+++ b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt
@@ -0,0 +1,35 @@
+--TEST--
+Expect 100-continue behavior in PHP development server (curl)
+--SKIPIF--
+<?php
+include "skipif.inc";
+?>
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+include 'php_cli_server.inc';
+$server = php_cli_server_start();
+
+// Generate a POST body larger than 1MB to trigger Expect: 100-continue
+$body = str_repeat('A', 1024 * 1024 + 1);
+
+$ch = curl_init();
+curl_setopt($ch, CURLOPT_URL, PHP_CLI_SERVER_ADDRESS);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
+
+// Set a high timeout for 100-continue response
+curl_setopt($ch, CURLOPT_EXPECT_100_TIMEOUT_MS, 2000);
+
+curl_exec($ch);
+var_dump(curl_errno($ch));
+
+echo "Did the PHP development server send a HTTP/1.1 100 Continue header?\n";
+$start_transfer_time = curl_getinfo($ch, CURLINFO_STARTTRANSFER_TIME_T);
+var_dump($start_transfer_time < 1_000_000);
+?>
+--EXPECT--
+int(0)
+Did the PHP development server send a HTTP/1.1 100 Continue header?
+bool(true)
diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt
new file mode 100644
index 00000000000..c792630a930
--- /dev/null
+++ b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Failure to send "100 Continue" is reported with ignore_user_abort=1
+--SKIPIF--
+<?php
+include "skipif.inc";
+if (!extension_loaded("sockets")) die("skip sockets extension required");
+if (PHP_OS_FAMILY === "Windows") die("skip SO_LINGER reset behaviour differs on Windows");
+?>
+--FILE--
+<?php
+include "php_cli_server.inc";
+$server = php_cli_server_start('echo "Hello world";', 'index.php', ['-d', 'ignore_user_abort=1']);
+
+$fp = fsockopen(PHP_CLI_SERVER_HOSTNAME, PHP_CLI_SERVER_PORT);
+socket_set_option(socket_import_stream($fp), SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0]);
+fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n");
+fclose($fp);
+
+$output = '';
+for ($i = 0; $i < 100 && !str_contains($output, 'Invalid request'); $i++) {
+ usleep(50000);
+ $output = file_get_contents($server->outputFile);
+}
+
+var_dump(str_contains($output, 'Invalid request'), str_contains($output, 'Unexpected EOF'));
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/php_cli_server_expect_100_continue_iua.log')
+?>
+--EXPECT--
+bool(true)
+bool(false)
diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt
new file mode 100644
index 00000000000..0d386155721
--- /dev/null
+++ b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt
@@ -0,0 +1,86 @@
+--TEST--
+Expect 100-continue behavior in PHP development server (sockets)
+--SKIPIF--
+<?php
+include "skipif.inc";
+?>
+--FILE--
+<?php
+include "php_cli_server.inc";
+php_cli_server_start();
+
+echo "# Send Expect: 100-continue header, receive 100 Continue response.\n";
+$fp = php_cli_server_connect();
+fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n");
+echo fgets($fp);
+echo fgets($fp);
+fwrite($fp, "body");
+echo fgets($fp);
+fclose($fp);
+
+echo "# Send Expect: 100-continue header on HTTP/1.0.\n";
+$fp = php_cli_server_connect();
+fwrite($fp, "POST / HTTP/1.0\r\nExpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n");
+$read = [$fp];
+var_dump(stream_select($read, $write, $except, 0, 1000));
+fwrite($fp, "body");
+echo fgets($fp);
+fclose($fp);
+
+echo "# Send Expect: 100-continue header and disconnect.\n";
+$fp = php_cli_server_connect();
+if (extension_loaded('sockets')) {
+ // Set SO_LINGER timeout to zero so that send fails on the server immediately
+ socket_set_option(
+ socket_import_stream($fp),
+ SOL_SOCKET,
+ SO_LINGER,
+ [
+ 'l_onoff' => 1,
+ 'l_linger' => 0,
+ ]
+ );
+}
+stream_socket_shutdown($fp, STREAM_SHUT_RD);
+fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n");
+fclose($fp);
+
+$fp = php_cli_server_connect();
+fwrite($fp, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n");
+echo fgets($fp);
+fclose($fp);
+
+echo "# GET with Expect header (no body).\n";
+$fp = php_cli_server_connect();
+fwrite($fp, "GET / HTTP/1.1\r\nExpect: 100-continue\r\nConnection: close\r\n\r\n");
+echo fgets($fp);
+fclose($fp);
+
+echo "# POST with empty body.\n";
+$fp = php_cli_server_connect();
+fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
+echo fgets($fp);
+fclose($fp);
+
+echo "# Lower-case expect header.\n";
+$fp = php_cli_server_connect();
+fwrite($fp, "POST / HTTP/1.1\r\nexpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n");
+echo fgets($fp);
+fclose($fp);
+?>
+--EXPECT--
+# Send Expect: 100-continue header, receive 100 Continue response.
+HTTP/1.1 100 Continue
+
+HTTP/1.1 200 OK
+# Send Expect: 100-continue header on HTTP/1.0.
+int(0)
+HTTP/1.0 200 OK
+# Send Expect: 100-continue header and disconnect.
+HTTP/1.1 200 OK
+# GET with Expect header (no body).
+HTTP/1.1 200 OK
+# POST with empty body.
+HTTP/1.1 200 OK
+# Lower-case expect header.
+HTTP/1.1 100 Continue