Repository navigation
PHP_STREAM_OPTION_READ_TIMEOUT is also used for socket write timeouts #23978
Description
Description
stream_set_timeout() passes PHP_STREAM_OPTION_READ_TIMEOUT to the stream wrapper:
php-src/ext/standard/streamsfuncs.c
Lines 1494 to 1499 in 810ac6d
| #endif | |
| php_stream_error_operation_begin(); | |
| RETVAL_BOOL(PHP_STREAM_OPTION_RETURN_OK == php_stream_set_option(stream, PHP_STREAM_OPTION_READ_TIMEOUT, 0, &t)); | |
| php_stream_error_operation_end_for_stream(stream); | |
| } |
The constant and its comment explicitly describe a read timeout:
Lines 448 to 450 in 810ac6d
| /* set the timeout duration for reads on the stream. ptrparam is a pointer to a struct timeval * */ | |
| #define PHP_STREAM_OPTION_READ_TIMEOUT 4 | |
| #define PHP_STREAM_OPTION_SET_CHUNK_SIZE 5 |
However, the native socket stream stores this option in the single sock->timeout field:
php-src/main/streams/xp_socket.c
Lines 407 to 410 in 810ac6d
| case PHP_STREAM_OPTION_READ_TIMEOUT: | |
| sock->timeout = *(struct timeval*)ptrparam; | |
| sock->timeout_event = false; | |
| return PHP_STREAM_OPTION_RETURN_OK; |
That timeout is used by both php_sockop_read() and php_sockop_write(). In particular, a blocking write which encounters EAGAIN/EWOULDBLOCK waits for POLLOUT using the same timeout:
php-src/main/streams/xp_socket.c
Lines 65 to 128 in 810ac6d
| static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t count) | |
| { | |
| php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; | |
| ssize_t didwrite; | |
| struct timeval *ptimeout; | |
| if (!sock || sock->socket == -1) { | |
| return 0; | |
| } | |
| if (sock->timeout.tv_sec == -1) | |
| ptimeout = NULL; | |
| else | |
| ptimeout = &sock->timeout; | |
| retry: | |
| didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && ptimeout) ? MSG_DONTWAIT : 0); | |
| if (didwrite <= 0) { | |
| char *estr; | |
| int err = php_socket_errno(); | |
| if (PHP_IS_TRANSIENT_ERROR(err)) { | |
| if (sock->is_blocked) { | |
| int retval; | |
| sock->timeout_event = false; | |
| do { | |
| retval = php_pollfd_for(sock->socket, POLLOUT, ptimeout); | |
| if (retval == 0) { | |
| sock->timeout_event = true; | |
| break; | |
| } | |
| if (retval > 0) { | |
| /* writable now; retry */ | |
| goto retry; | |
| } | |
| err = php_socket_errno(); | |
| } while (err == EINTR); | |
| } else { | |
| /* EWOULDBLOCK/EAGAIN is not an error for a non-blocking stream. | |
| * Report zero byte write instead. */ | |
| return 0; | |
| } | |
| } | |
| if (!(stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS)) { | |
| estr = php_socket_strerror(err, NULL, 0); | |
| php_stream_warn(stream, NetworkSendFailed, | |
| "Send of %zu bytes failed with errno=%d %s", count, err, estr); | |
| efree(estr); | |
| } | |
| } | |
| if (didwrite > 0) { | |
| php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), didwrite, 0); | |
| } | |
| return didwrite; | |
| } |
This creates an ambiguity for extension and alternative stream-wrapper implementers: following the constant name and header comment produces a read-only implementation, while matching native PHP socket stream behavior requires applying the value to both blocking reads and blocking writes.
This ambiguity caused a real compatibility issue in Swoole's coroutine stream hook, where stream_set_timeout() changed the read timeout but a blocked fwrite() continued using the previous write timeout:
The PHP manual currently says only that the function “sets the timeout value on the stream”; it does not document that native network/socket streams use the value for writes as well, and its example demonstrates only a read timeout:
https://github.com/php/doc-en/blob/master/reference/stream/functions/stream-set-timeout.xml
Suggested changes:
- Clarify the
stream_set_timeout()documentation: for native network/socket streams, the configured timeout applies to blocking reads and blocking writes. - Update the misleading comment in
main/php_streams.h. - Introduce a direction-neutral internal name such as
PHP_STREAM_OPTION_IO_TIMEOUTorPHP_STREAM_OPTION_TIMEOUT, while retainingPHP_STREAM_OPTION_READ_TIMEOUTas an alias for source compatibility, and migrate in-tree users to the clearer name. - Consider adding a php-src regression test covering a blocked socket-stream write after
stream_set_timeout().
No runtime behavior change is being requested here; the goal is to make the existing behavior unambiguous and prevent downstream implementations from interpreting the option differently.
Activity