OxPHP 0.14.0:弃用变量被静默忽略,路由退回传统模式后文档根下任意 .php 都会执行
- 项目:
- 更新日志:
- 镜像:
ghcr.io/oxphp/oxphp:0.14.0、ghcr.io/oxphp/oxphp:0.14.0-php8.4 - 版本日期:2026-10-25
路由与变量弃用
INDEX_FILE、WORKER_FILE、PHP_DENY_DIRS、STATIC_CACHE_TTL、STATIC_CACHE 和 COMPRESSION_LEVEL 在 0.14.0 中不再被读取。遗留未删的设置不会触发任何警告。
后果在启动后直接反映到路由模式上:仍在使用 INDEX_FILE 或 WORKER_FILE 的框架模式、工作进程模式部署,会退回到传统模式。此时文档根目录下的任何 .php 文件都会执行,原先只在 PHP_DENY_DIRS 中列出的路径也会再次被提供。
迁移对应关系:
INDEX_FILE→ENTRY_FILEWORKER_FILE→ENTRY_FILE,并设置WORKER_MODE_ENABLED=truePHP_DENY_DIRS→PHP_DENY_PATHSSTATIC_CACHE_TTL→STATIC_MAX_AGESTATIC_CACHE→STATIC_REVALIDATECOMPRESSION_LEVEL→COMPRESSION_ENCODINGS搭配COMPRESSION_BROTLI_LEVEL
首次启动时检查日志中的 OxPHP routing mode decided,确认实际生效的模式。
WORKER_MAX_REQUESTS、SHARED_SHUTDOWN_TIMEOUT_SECONDS、SHARED_SOFT_LIMIT_RATIO、SHARED_POISON_STRICT 原本就没有效果,它们的启动 WARN 也被移除。替代方式:用 WORKER_MAX_MEMORY_MIB 或 Worker::scheduleExit() 回收工作进程,用 DRAIN_TIMEOUT_SECONDS 限定关闭时长,基于 oxphp_shared_capacity_saturation 设置告警,在工厂失败必须禁用 Shared\Once 的地方传入 Once\FailureMode::Poison。
未写入文档的 oxphp_pool_spike_capture()、oxphp_pool_spike_invoke()、oxphp_pool_spike_reset() 已移除。它们是内部探针,传入能到达 __call() 或 __callStatic() 的可调用对象并多次调用,会执行已释放的内存。
镜像标签与 Alpine 3.24
:latest、:0.14.0、:0.14.0-php8.4 以及其他所有未注明 Alpine 版本的标签,现在都基于 Alpine 3.24 构建。在 php:-zts-alpine3.23 上构建扩展、或把 OxPHP 复制到该基础镜像上的 Dockerfile,必须迁移到 alpine3.24,或固定使用 :0.14.0-php-alpine3.23。在 3.23 上构建的 intl.so 需要 libicuuc.so.76,3.24 中没有这个库。
每个版本也发布 Debian(glibc)镜像,基于 Debian trixie,支持 linux/amd64 和 linux/arm64。标签规则为该版本 Alpine 标签末尾加上 -trixie,用 :trixie 代替 :latest。文档中基于 php:8.4-zts-alpine 的安装、Docker 示例已改为从 -php8.4 标签获取 OxPHP,而不是默认的 PHP 8.5 版本。
请求头与 $_SERVER
带 _ 或 . 的请求头名称不再进入 $_SERVER。客户端发送 X_Api_Key 时,读取 $_SERVER['HTTP_X_API_KEY'] 的应用读不到任何值。应改用 oxphp_http_request()->header('X_Api_Key') 读取,或让客户端发送 X-Api-Key。
这一条同时是安全修复:X_Forwarded_For 和 X.Authenticated.User 过去会以 HTTP_X_FORWARDED_FOR、HTTP_X_AUTHENTICATED_USER 到达,替换或创建代理设置的键,除非代理剥离了它们。现在这类请求头不再进入 $_SERVER。
重复的请求头行会被合并。以多行发送的请求头用 ", " 连接成一个值传给 PHP,因此在两层代理之后,HTTP_X_FORWARDED_FOR 会同时包含两行,而过去只包含最后一行。Content-Type、Host、Content-Length 取第一行,所以 HTTP_HOST 现在是第一行 Host。任意一行含非 ASCII 字节的请求头会被整个略去。
HTTP_HOST、SERVER_NAME、SERVER_PORT、host()、port()、fullUri() 现在取自 HTTP/2 的 :authority 以及 HTTP/1.x 的绝对形式目标。过去通过 HTTP/2 访问的浏览器到达时没有 HTTP_HOST,SERVER_NAME 被设为 localhost。与 :authority 不一致的 Host、空主机、格式错误的目标(GET index.php、不以 / 开头的 :path、OPTIONS 之外的 *)会直接以 400 应答,不运行 PHP。
CONNECT 以 501 应答,不再运行 PHP,也不再到达静态文件。探测开放代理的请求现在计入 oxphp_responses_by_status_total{status="5xx"} 而不是 2xx;启用 OTEL_ENABLED 时,其服务器 span 标记为错误。
会话与 putenv 泄漏
工作进程模式在每个请求结束时撤销 putenv()。每个请求都应看到的值必须在 oxphp_worker() 之前设置。在请求内部设置的值,包括首个请求时才惰性运行的引导代码设置的值,都会在该请求结束时消失。
安全影响不止于此:某个请求用 putenv() 设置的值(例如客户端的令牌)过去会在每个工作进程此后的每个请求中通过 getenv() 被读到,并被子进程继承,直到工作进程被回收。现在会在请求结束时恢复。
工作进程模式一次只持有一个打开的会话。某个请求在会话打开状态下暂停时(oxphp_async_await()、oxphp_sleep() 中,或在 RUNTIME_HOOKS 下进行 MySQL / Redis 查询时),其工作进程不接收新请求,其他请求中的 session_start() 会等待。会话数据确定之后、挂起之前应调用 session_write_close()。
会话串用问题也已修复:当另一个客户端的请求在会话打开时暂停,此时被接收或恢复的请求会拿到那个会话;在 session_write_close() 之后,下一次 session_start() 可能取用已关闭会话的 id,并在 Set-Cookie 中把它发回,从而让一个客户端以另一个客户端的身份登录。类似地,为 session.use_trans_sid 或 output_add_rewrite_var() 重写的链接,可能携带另一请求暂停期间所处理请求的会话 id。
未 await 的 oxphp_async() 任务会随请求一同取消。即使任务从不挂起,也会在引擎下一次检查中断的位置以 OxPHP\Async\AsyncException 展开。必须完成的工作必须 await。
响应缓冲溢写 TMPDIR
未以流式发送、且超出 RESPONSE_BUFFER_BYTES(1 MiB)的响应,会被写入 TMPDIR(未设置时为 /tmp)中的临时文件。在只读根文件系统上,需要挂载一个可写卷并让 TMPDIR 指向它,否则每个这样的响应都会是 503。
这些文件在容器可写层上占用磁盘(Kubernetes 将其计为 ephemeral-storage),在 tmpfs 上占用内存。RESPONSE_SPOOL_MAX_BYTES(1 GiB)限制它们的总量,超出后应答为 503 并附带 Retry-After: 5。
慢速客户端相关的问题也一并修复。通过 HTTP/1.1 时,服务器过去会尽可能快地读取静态文件或流式响应,不管客户端接收多快:十个以 1 MiB/s 读取 500 MiB 文件的请求,在两秒内就让容器超过 6 GiB 内存限制。现在写缓冲区满时会停止读取。脚本输出过去会留在内存中(不计入 memory_limit)直到最后一个字节被读取,因此十个 500 MiB 文件的慢速下载会占住约 5 GiB;缓冲响应现在不会保留到客户端读完。
Shared\Pool 生命周期
在工作进程模式之外,Registry::pool() 会抛出 OxPHP\Shared\SharedException,池会在创建它的请求结束时关闭。以这种方式跨请求保留的连接池会交出 PHP 已释放的内存。应改用 PDO::ATTR_PERSISTENT 或工作进程模式。来自另一个请求、来自任务、或在请求结束之后的获取会抛出 ClosedException。oxphp run 和工作进程模式不受影响。
在其他线程上运行的池回调拒绝对象:在工作进程模式下,以及对于在 oxphp_async() 任务中创建的池,由另一个 PHP 线程运行的工厂或销毁回调如果捕获了对象、闭包或资源,或把它们保存在 static 中,或使用了 $this,就会抛出一个指明该变量的 Error。其他被捕获的值和 static 值按池创建时的状态复制。
池回调随创建它的请求一同结束:一旦创建池的请求结束(其工作进程被回收、退役或停止),需要新建槽位的获取会抛出 ClosedException;其他线程销毁的槽位不会运行 $destroy,并计入 oxphp_shared_pool_destroy_skipped_total{reason="request_ended"}。
工作进程模式下现在会驱逐空闲槽位:Shared\Pool 会销毁空闲时间超过 idleTimeoutMs 的槽位,并调用 $destroy,连接也不例外。过去它们会一直保留到 evict() 或服务器停止。
$destroy 只在所属线程上运行:空闲超时驱逐、evict() 以及最后一个引用消失时,都在持有该槽位的线程上执行。未经它就离开的槽位计入 oxphp_shared_pool_destroy_skipped_total,reason 为 no_frame、other_thread 或 request_ended。ClosedException 不再被弃用。
获取不再占住线程:等待槽位会挂起请求或任务,并每隔几毫秒检查一次是否有空闲槽位。过去它会阻塞线程,当同一工作进程上的另一个请求持有该槽位时可能挂起。命名参数问题也已修复:new OxPHP\Shared\Pool(factory: $make, maxSize: 8)、只给出 failure: 而未给出 success: 的 compareAndSet()、给出 span_id: 的 oxphp_apm_event() 或 oxphp_apm_status() 不再抛出 ArgumentCountError。
RUNTIME_HOOKS 下的异常与事务
RUNTIME_HOOKS 包含 streams 时,如果某个函数的本地 pdo_mysql 连接有打开的事务,或有未读完的非缓冲语句,从该函数抛出的异常会跳过其 catch,转而执行调用之后的代码;在那里调用 exit() 会让脚本继续运行。现在异常会在等待期间暂存,并在等待之后放回。
持久 PDO 事务也有修复:在 PHP 8.4.27 / 8.5.12 及更高版本上使用 RUNTIME_HOOKS=streams 时,如果一个请求在事务进行中放开了它的 PDO 句柄,而该持久连接还被其他请求的句柄共享,事务会为该连接的下一个使用者保持打开,后者可能会提交它。现在 OxPHP 会自行发送回滚。
内存耗尽不再拖垮服务器:在 PHP 8.4.27 / 8.5.12 及更高版本上,超过 memory_limit 的请求或任务过去在 8.4 上让服务器崩溃,在 8.5 上让工作进程此后只返回空响应。现在它以致命错误结束,关闭函数会运行,工作进程继续提供服务。PHP 新的硬内存限制不在此列:超出它的分配会以退出码 137 结束进程。
异步线程上的 OPcache 表:运行 OPcache 在其线程启动后才缓存的代码的任务或池回调,可能越过该线程表的末尾进行读写,结果为 [CRASH] SIGSEGV 或 SIGBUS。现在线程会在运行该代码之前扩展其表。
await 超时过大时,过去传给 oxphp_async_await() 或其变体的 INF、1e300、PHP_INT_MAX 会让服务器中止,在 arm64 上则立即超时。现在只有当超时大于 0 且不超过 1e9 秒时才设置时限。
OxPHP 自身代码中的 panic,如果在有请求打开时展开了 PHP 工作线程,或展开了异步池线程,现在会以 oxphp: thread '' panicked with a PHP request open; aborting 中止进程。以 PID 1 运行时容器以 139 退出,使用 docker run --init 时为 134。
工作进程模式下,等待慢速客户端的刷新(其 64 个块的队列已满)会像 oxphp_sleep() 一样挂起请求,而不是占住工作线程及其上的每个请求。所有模式下,该等待现在都会在 max_execution_time 到达时以及优雅关闭开始时结束。
升级检查清单
- 把
INDEX_FILE、WORKER_FILE迁移到ENTRY_FILE,需要工作进程模式时加WORKER_MODE_ENABLED=true;启动后确认OxPHP routing mode decided日志。 - 把
PHP_DENY_DIRS改为PHP_DENY_PATHS,确认文档根下没有本不该暴露的.php。 STATIC_CACHE_TTL→STATIC_MAX_AGE,STATIC_CACHE→STATIC_REVALIDATE,COMPRESSION_LEVEL→COMPRESSION_ENCODINGS+COMPRESSION_BROTLI_LEVEL。- 检查
TMPDIR指向可写卷;确认RESPONSE_BUFFER_BYTES、RESPONSE_SPOOL_MAX_BYTES与磁盘和内存预算匹配。 - Dockerfile 基础镜像从
alpine3.23迁到alpine3.24,或固定:0.14.0-php-alpine3.23;使用 intl 扩展时注意libicuuc.so.76。 - 读取带
_或.的请求头改走oxphp_http_request()->header();检查代理是否合并了重复的头行。 - 会话数据确定后、挂起前调用
session_write_close();不要依赖请求内putenv()或未 await 的任务。 - 检查
Shared\Pool是否在工作进程模式之外跨请求使用;处理oxphp_shared_pool_destroy_skipped_total的新 reason 值。 - 升级到 PHP 8.4.27 / 8.5.12 以上以获取 PDO 事务与内存耗尽相关修复。