Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.15.0] - 2026-08-26

### Added

- **A TCP listener can ask the kernel for a port, and say which one it got.** `addListener()`, `addHttp1Listener()` and `addHttp2Listener()` take port 0, which binds to whatever the kernel assigns, and `HttpServer::getBoundListeners()` reports what the server actually holds — one entry per configured listener, in configuration order, empty while the server is not running. Naming a free port before binding it leaves a window in which the port belongs to nobody: the phpt suite picks ports that way and loses the race under `-j4`, where a second server's `start()` answers `Failed to acquire TCP listener for 127.0.0.1:58864 (bind)`. Port 0 removes the window rather than narrowing it. Across several threads such a listener is bound once into the shared set and every thread adopts a duplicate, SO_REUSEPORT or not — binding per thread would hand each a different port. HTTP/3 still requires an explicit port: it binds through the UDP path, which reports no local address, so 0 is refused there rather than answered with a guess.
Expand Down Expand Up @@ -1446,7 +1448,9 @@ on the [TrueAsync](https://github.com/true-async) event loop.
and Windows, quick start), `docs/` (coding standards, contributor
recommendations, llhttp upstream notes), Apache 2.0 `LICENSE`.

[Unreleased]: https://github.com/true-async/server/compare/v0.13.0...HEAD
[Unreleased]: https://github.com/true-async/server/compare/v0.15.0...HEAD
[0.15.0]: https://github.com/true-async/server/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/true-async/server/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/true-async/server/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/true-async/server/compare/v0.11.3...v0.12.0
[0.11.3]: https://github.com/true-async/server/compare/v0.11.2...v0.11.3
Expand Down
39 changes: 33 additions & 6 deletions ide-stubs/true-async-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -431,12 +431,21 @@ public function setSymlinkPolicy(StaticSymlinks $policy): static {}
*
* Patterns are matched against the path *relative to the root
* directory*, with `/` as the separator, by gitignore's rule:
* a pattern naming no directory names a file, and covers a file
* of that name at any depth, so `*.php` hides `index.php` and
* `admin/tools.php` alike. A pattern naming a directory is
* anchored at the root and `*` stops at each separator, so
* `cache/*` hides the mount's own `cache/` and not `var/cache/`.
*
* `*.php` a file of that name at any depth
* `/index.php` that file at the root directory only
* `cache/x` anchored at the root directory, `*` stopping at each `/`
* `cache/` a directory of that name at any depth, and all it holds
* `cache/**` anchored, `*` crossing `/`
*
* A pattern opening with a double star names every directory, the root
* directory among them. A pattern without `/` reads the file name, so a
* directory named like it keeps serving what it holds — `cache/` is how to
* cover a directory. Case follows the platform's own filesystem:
* case-insensitive on Windows, case-sensitive elsewhere.
*
* @throws HttpServerInvalidArgumentException when a pattern is empty or
* longer than 512 bytes.
* @return static
*/
public function hide(string ...$globs): static {}
Expand Down Expand Up @@ -558,7 +567,8 @@ public function __construct(?string $host = null, int $port = 8080) {}
* {@see addHttp3Listener()}.
*
* @param string $host Host to bind (e.g., "127.0.0.1", "0.0.0.0")
* @param int $port Port to listen on
* @param int $port Port to listen on, or 0 to take whatever the kernel
* gives — {@see HttpServer::getBoundListeners()} reports it
* @param bool $tls Enable TLS for this listener
* @return static
*/
Expand Down Expand Up @@ -1795,6 +1805,23 @@ public function reload(): bool {}
*/
public function isRunning(): bool {}

/**
* The listeners the server actually holds, one entry per configured
* listener, in configuration order.
*
* A listener configured with port 0 is bound to a port the kernel picks,
* and the entry carries that port: there is no gap between choosing an
* address and owning it, which a caller picking a free port beforehand
* cannot avoid. HTTP/3 listeners still require an explicit port and
* report the configured one.
*
* Empty while the server is not running: before start(), after stop().
*
* @return array<int, array{type: 'tcp'|'udp_h3', host: string, port: int, tls: bool}
* |array{type: 'unix', path: string}>
*/
public function getBoundListeners(): array {}

/**
* Whether the extension was built with HTTP/2 support (--enable-http2).
*/
Expand Down
2 changes: 1 addition & 1 deletion include/php_http_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ static zend_always_inline bool http_path_is_cwd_independent(const char *path, si
* static-build codegen (genif.sh) can discover it; see the comment there. */
#include "php_true_async_server.h"

#define PHP_HTTP_SERVER_VERSION "0.14.0"
#define PHP_HTTP_SERVER_VERSION "0.15.0"

/*
* ==========================================================================
Expand Down
Loading