From 9cc4fbcda6de4a6dbbb9a7129cf830b358ca67a6 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 13 Aug 2026 13:11:32 +0000 Subject: [PATCH 1/4] 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. - https://everything.curl.dev/http/post/expect100.html - https://github.com/php/php-src/issues/23242 --- .../curl_expect_100_continue_timeout.phpt | 31 +++++++++++++++++++ sapi/cli/php_cli_server.c | 20 ++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 ext/curl/tests/curl_expect_100_continue_timeout.phpt diff --git a/ext/curl/tests/curl_expect_100_continue_timeout.phpt b/ext/curl/tests/curl_expect_100_continue_timeout.phpt new file mode 100644 index 000000000000..105f7e5e658a --- /dev/null +++ b/ext/curl/tests/curl_expect_100_continue_timeout.phpt @@ -0,0 +1,31 @@ +--TEST-- +curl with expect 100-continue against PHP development server +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +int(0) +Did the PHP development server send a HTTP/1.1 100 Continue header? +bool(true) diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 007718e37b9c..8652738fd932 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -175,6 +175,7 @@ typedef struct php_cli_server_client { php_http_parser parser; bool request_read; bool too_large_post; + bool expect_continue; zend_string *current_header_name; zend_string *current_header_value; enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element; @@ -1791,6 +1792,12 @@ 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")) { + client->expect_continue = true; + } + return 0; } @@ -1886,6 +1893,18 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha } client->parser.data = client; nbytes_consumed = php_http_parser_execute(&client->parser, &settings, buf, nbytes_read); + 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. */ + smart_str buffer = { 0 }; + append_http_status_line(&buffer, client->parser.http_major * 100 + client->parser.http_minor, 100, 0); + smart_str_appendl(&buffer, "\r\n", 2); + smart_str_0(&buffer); + php_cli_server_client_send_through(client, ZSTR_VAL(buffer.s), ZSTR_LEN(buffer.s)); + smart_str_free(&buffer); + client->expect_continue = false; + } if (nbytes_consumed != (size_t)nbytes_read && !client->too_large_post) { if (php_cli_server_log_level >= PHP_CLI_SERVER_LOG_ERROR) { if ((buf[0] & 0x80) /* SSLv2 */ || buf[0] == 0x16 /* SSLv3/TLSv1 */) { @@ -1981,6 +2000,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se php_http_parser_init(&client->parser, PHP_HTTP_REQUEST); client->request_read = false; client->too_large_post = false; + client->expect_continue = false; client->last_header_element = HEADER_NONE; client->current_header_name = NULL; From 0052e125d273e57e1f4182af5e4e65f6c6c1f812 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Fri, 14 Aug 2026 14:40:25 +0000 Subject: [PATCH 2/4] Move test to sapi/cli This is not a curl test, but a test of the behavior of the PHP development server. It should thus not be in the curl directory but in the sapi/cli directory. --- .../cli/tests/php_cli_server_expect_100_continue.phpt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename ext/curl/tests/curl_expect_100_continue_timeout.phpt => sapi/cli/tests/php_cli_server_expect_100_continue.phpt (80%) diff --git a/ext/curl/tests/curl_expect_100_continue_timeout.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue.phpt similarity index 80% rename from ext/curl/tests/curl_expect_100_continue_timeout.phpt rename to sapi/cli/tests/php_cli_server_expect_100_continue.phpt index 105f7e5e658a..3f7117a49812 100644 --- a/ext/curl/tests/curl_expect_100_continue_timeout.phpt +++ b/sapi/cli/tests/php_cli_server_expect_100_continue.phpt @@ -1,17 +1,17 @@ --TEST-- -curl with expect 100-continue against PHP development server +Expect 100-continue behavior in PHP development server --EXTENSIONS-- curl --FILE-- Date: Fri, 21 Aug 2026 08:57:49 +0000 Subject: [PATCH 3/4] Add expect/continue test based on sockets --- ..._cli_server_expect_100_continue_curl.phpt} | 6 +- ...cli_server_expect_100_continue_socket.phpt | 62 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) rename sapi/cli/tests/{php_cli_server_expect_100_continue.phpt => php_cli_server_expect_100_continue_curl.phpt} (88%) create mode 100644 sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt similarity index 88% rename from sapi/cli/tests/php_cli_server_expect_100_continue.phpt rename to sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt index 3f7117a49812..37886fc25e1e 100644 --- a/sapi/cli/tests/php_cli_server_expect_100_continue.phpt +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt @@ -1,5 +1,9 @@ --TEST-- -Expect 100-continue behavior in PHP development server +Expect 100-continue behavior in PHP development server (curl) +--SKIPIF-- + --EXTENSIONS-- curl --FILE-- 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 000000000000..33249787e0cd --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt @@ -0,0 +1,62 @@ +--TEST-- +Expect 100-continue behavior in PHP development server (sockets) +--SKIPIF-- + +--FILE-- + +--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 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 From cf03680d4a9bb036e503d73bbfecb77b4580cf45 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Fri, 21 Aug 2026 20:10:08 +0000 Subject: [PATCH 4/4] Add error handling If php_cli_server_client_send_through fails while sending `HTTP/1.1 100 Continue` we don't want the server to exit. --- sapi/cli/php_cli_server.c | 10 +++++++++- .../php_cli_server_expect_100_continue_socket.phpt | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 8652738fd932..335fc0badffd 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -1898,12 +1898,20 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha * finished reading the body. Send 100 Continue before the client * sends the request body. */ smart_str buffer = { 0 }; + bool send_failed = true; append_http_status_line(&buffer, client->parser.http_major * 100 + client->parser.http_minor, 100, 0); smart_str_appendl(&buffer, "\r\n", 2); smart_str_0(&buffer); - php_cli_server_client_send_through(client, ZSTR_VAL(buffer.s), ZSTR_LEN(buffer.s)); + zend_try { + php_cli_server_client_send_through(client, ZSTR_VAL(buffer.s), ZSTR_LEN(buffer.s)); + send_failed = false; + } zend_end_try(); smart_str_free(&buffer); client->expect_continue = false; + if (send_failed) { + *errstr = php_socket_strerror(php_socket_errno(), NULL, 0); + return -1; + } } if (nbytes_consumed != (size_t)nbytes_read && !client->too_large_post) { if (php_cli_server_log_level >= PHP_CLI_SERVER_LOG_ERROR) { 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 index 33249787e0cd..ddd7d50d28f7 100644 --- a/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt @@ -20,6 +20,18 @@ 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);