Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8533afa
feat(grpc): gRPC over HTTP/2 — unary + all streaming shapes (#4)
EdmondDantes Jul 4, 2026
4e638a7
feat(grpc): per-message gzip compression (#4)
EdmondDantes Jul 4, 2026
afc1f63
feat(grpc): grpc-web (binary) — in-body 0x80 trailer frame (#4)
EdmondDantes Jul 4, 2026
93e58b5
docs(grpc): H3 gRPC feasibility + ready-to-execute Phase 5b plan
EdmondDantes Jul 4, 2026
cb5b157
feat(grpc): gRPC over HTTP/3 — native trailers via nghttp3 + grpc-web…
EdmondDantes Jul 4, 2026
e4d7a3b
feat(grpc): true full-duplex bidi — incremental readMessage + body-ca…
EdmondDantes Jul 4, 2026
3ba71e1
refactor(grpc): extract call lifecycle policy from H2/H3 transports (#4)
EdmondDantes Jul 6, 2026
98adc6c
fix(fuzz): stub http_response_get_trailers for the h2 session harness
EdmondDantes Jul 6, 2026
84022b4
ci: unskip gRPC coverage paths — zlib ext + aioquic in the phpt job
EdmondDantes Jul 6, 2026
33fb327
docs(grpc): CHANGELOG entry, README feature row + Quick Start; cover …
EdmondDantes Jul 6, 2026
b38301e
feat(pool): response trailers cross the reactor/worker split (#80, #4)
EdmondDantes Jul 6, 2026
e12a691
feat(pool): streaming reverse path — send()/writeMessage() cross the …
EdmondDantes Jul 6, 2026
7520661
feat(pool): credit-based backpressure for the streaming reverse path …
EdmondDantes Jul 6, 2026
afe33eb
feat(grpc): grpc-web-text — per-frame base64 both directions (#4)
EdmondDantes Jul 6, 2026
d795ae0
docs: gRPC 100% — grpc-web-text + reactor-pool streaming landed (#4, …
EdmondDantes Jul 6, 2026
ef4203e
feat(h3): streaming request bodies — issue-#26 policy on HTTP/3 + upl…
EdmondDantes Jul 6, 2026
f81e58d
ci: coverage-drop-ok — http3_dispatch pool-only paths are gcov-invisible
EdmondDantes Jul 6, 2026
bf09e51
fix: review P1 — web-text decode, body_h3_conn UAF, streaming policy/…
EdmondDantes Jul 7, 2026
bbcc388
refactor: review P2 — one handler-pick seam, classify-once gRPC, shar…
EdmondDantes Jul 7, 2026
84b7c52
perf: review P3 — reverse-path copy/wake/drain costs
EdmondDantes Jul 7, 2026
6fbe731
chore: review P4 — drop dead state, fix stale contracts
EdmondDantes Jul 7, 2026
51c0823
fix(h2): sever body_h2_session at stream teardown (UAF class, H2 mirr…
EdmondDantes Jul 7, 2026
07930e6
test(pool): reactor-pool phpts exit via $server->stop(), not SIGKILL
EdmondDantes Jul 7, 2026
3fdd255
Merge remote-tracking branch 'origin/main' into 4-grpc-support-http2-…
EdmondDantes Jul 7, 2026
f06203a
test: worker-pool phpts exit via $server->stop() where it works
EdmondDantes Jul 7, 2026
dfe1680
test: static/004 exits via stop() too — the 'abort' was the experimen…
EdmondDantes Jul 7, 2026
43454ea
docs: state the ABORT rationale once (at the wire kind), not at four …
EdmondDantes Jul 7, 2026
080c402
#104: ERRORS.md
EdmondDantes Jul 7, 2026
290de99
Merge remote-tracking branch 'origin/4-grpc-support-http2-http3' into…
EdmondDantes Jul 7, 2026
0b95fc8
docs: fix ERRORS2.md — remove duplicate entry, correct line numbers/a…
EdmondDantes Jul 7, 2026
5840e51
refactor(grpc): apply ERRORS.md review — collapse classification, dro…
EdmondDantes Jul 7, 2026
b22bafa
Merge branch '4-grpc-support-http2-http3' of https://github.com/true-…
EdmondDantes Jul 7, 2026
5fbfeba
perf(http3): apply ERRORS2.md reactor review
EdmondDantes Jul 8, 2026
9dfa5eb
fix(http): three latent use-after-free bugs found under ASAN
EdmondDantes Jul 8, 2026
ad3949f
docs(comments): trim branch comments to contracts and constraints
EdmondDantes Jul 8, 2026
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
10 changes: 10 additions & 0 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ jobs:
--enable-pcntl \
--enable-opcache \
--with-openssl \
--with-zlib \
--with-config-file-path=/etc \
--with-config-file-scan-dir=/etc/php.d \
--${{ matrix.debug && 'enable' || 'disable' }}-debug \
Expand Down Expand Up @@ -518,6 +519,15 @@ jobs:
ulimit -c unlimited
ulimit -c

# H3 gRPC tests (grpc/011, grpc/012) drive the server with a real
# aioquic Python client and SKIP when the module is missing —
# which silently drops the H3 trailer paths from coverage.
- name: Install aioquic (H3 gRPC client tests)
run: |
set -eux
python3 -m pip install --quiet aioquic
python3 -c "import aioquic"

- name: Run http_server phpt suite
working-directory: http-server
run: |
Expand Down
70 changes: 70 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **gRPC over HTTP/2 and HTTP/3 (#4).** Requests whose content-type begins with
`application/grpc` route to the callable registered via
`HttpServer::addGrpcHandler()`; everything else is untouched, so gRPC and
regular HTTP handlers coexist on one listener.
- **All four RPC shapes** — unary, server-streaming, client-streaming and
true full-duplex bidi: `HttpRequest::readMessage()` deframes the request
stream incrementally (5-byte length-prefix framing, 16 MiB per-message cap),
`HttpResponse::writeMessage()` frames replies; the handler starts on
HEADERS, before the body finishes.
- **Trailers**: `grpc-status`/`grpc-message` ride real HTTP trailers on both
transports (nghttp2 trailer HEADERS; `nghttp3_conn_submit_trailers` at true
EOF on H3 — verified with a real aioquic client). `grpc-status: 0` is
defaulted on success, `13 INTERNAL` on an uncaught handler exception, and a
handler that writes no messages gets the canonical Trailers-Only reply.
- **grpc-web (binary)**: `application/grpc-web` calls carry their trailers
in-body as a `0x80`-flagged frame, on H2 and H3.
- **Per-message gzip**: inbound `grpc-encoding: gzip` messages inflate
transparently in `readMessage()`; `writeMessage(..., compress: true)`
emits compressed frames.
- **`grpc-timeout`** request header parsed and exposed via
`HttpRequest::getGrpcTimeout()`.
- **grpc-web-text**: `application/grpc-web-text` calls carry base64 both
directions — `readMessage()` decodes the request transparently, every
response frame (messages + the trailer frame) goes out independently
base64-encoded.
- **Works under the reactor pool** (`TRUE_ASYNC_SERVER_REACTOR_POOL=1`) —
gRPC rides the generic streaming reverse path below; no gRPC-specific
code in the reactor/worker split.

- **Reactor-pool streaming reverse path (#80).** Under
`TRUE_ASYNC_SERVER_REACTOR_POOL=1` a worker response is no longer
buffered-only:
- `send()`/`writeMessage()`/SSE stream across the thread boundary — the
worker posts STREAM_HEADERS / STREAM_CHUNK / STREAM_END wires in FIFO
order; the reactor feeds its existing chunk ring and submits native
trailers at true EOF (so `setTrailer()` works under the pool, buffered
or streamed).
- **Credit-based backpressure**: a per-stream credit block (atomics,
malloc-domain) paces the producer — over 1 MiB un-acked in flight the
handler coroutine parks and resumes as the QUIC peer acknowledges
bytes, so a slow client cannot flood the shared reactor mailbox. Peer
RST / connection close unparks it into the standard stream-dead path
(`send()` throws 499).

- **HTTP/3 streaming request bodies (#26 policy on H3).** With
`setBodyStreamingEnabled(true)` the H3 dispatch now applies the same
three-case Content-Length policy as HTTP/2, so `readBody()` and
incremental `readMessage()` (true full-duplex gRPC) work over HTTP/3.
QUIC flow-control credit is deferred: the window refills as the handler
drains chunks, bounding un-read bytes by `max_body_size`.

### Fixed

- **HTTP/3 uploads larger than the initial stream window (256 KiB default)
stalled forever.** `nghttp3_conn_read_stream`'s consumed count excludes
DATA payload by contract, and nothing extended the QUIC windows for
buffered body bytes — now `h3_recv_data_cb` returns the credit as it
consumes them.

### Changed

- **gRPC layering: call-lifecycle policy extracted out of the transports (#4).**
`src/grpc/grpc_call.c` owns response defaults, outcome → `grpc-status` and
delivery shape (grpc-web in-body frame / streaming EOF / Trailers-Only);
HTTP/2 and HTTP/3 provide a 3-op wire vtable and stay gRPC-agnostic on
delivery. H3 response-trailer capture/submit is now generic — any streaming
response with a trailer map is delivered, not just gRPC (parity with H2).

## [0.9.2] - 2026-07-03

### Added
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ set(CORE_SOURCES
src/http_server_exceptions.c
src/http_request.c
src/http_response.c
src/grpc/grpc.c
src/grpc/grpc_call.c
src/http_sse.c
src/uploaded_file.c
src/http_mime.c
Expand Down
25 changes: 25 additions & 0 deletions ERRORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
1. Комментари в коде говно!!!! Убрать лишние комментарии, которые не несут смысловой нагрузки!
2. http_compression_message.h - отдельный модуль ради двуз функций? оверинженеринг!!!
if (req == NULL || req->headers == NULL) {
return false;
}

3. в коде много проверок, которые скорее всего не нужны
4. имена параметров говно: zval *ct = zend_hash_str_find(req->headers, "content-type", - что такое ct?
5. grpc_request_is_grpc и grpc_request_is_grpc_web - явное дублирование кода. и не только!

6. Максимально grpc_request_mode не эффективный код!!!! Можно было бы сперва получить строку потом сравнить в switch
7. grpc_web_text_decode - можно ли предсказать буфер? примерно?
8. Куча smart_str_appendl
9. switch (s[n - 1]) {
case 'H': unit_ns = 3600ULL * 1000000000ULL; break; /* hours */
case 'M': unit_ns = 60ULL * 1000000000ULL; break; /* minutes */
case 'S': unit_ns = 1000000000ULL; break; /* seconds */
case 'm': unit_ns = 1000000ULL; break; /* millis */
case 'u': unit_ns = 1000ULL; break; /* micros */
case 'n': unit_ns = 1ULL; break; /* nanos */
default: return 0;
пахнет отдельной общей функцией

10. grpc_message_inflate можно вообще в коде не определять и не вызывать если нет поддержки сжатия
11.
28 changes: 28 additions & 0 deletions ERRORS2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# HTTP/3 реакторы — находки

1. Модуль: `src/core/reactor_pool.c` — Функция: `reactor_pool_msleep` (строки 95-103), используется в `reactor_pool_exec` (289-324) и `reactor_pool_destroy` (367-392) — busy-wait поллинг (Sleep(1)/nanosleep в цикле) вместо condvar/event. Причина, видимо: `thread_mailbox_t` — lock-free кольцевой буфер без мьютекса; городить condvar (плюс мьютекс, плюс разный API POSIX/Win32) ради этого пути не стали. НЕ на hot path запроса — вызывается только при старте/остановке listener'а (`http_server_class.c:2371,2411`) и в тестах, так что 1мс задержка не критична сейчас. Но конструкция не согласуется с остальным lock-free дизайном — если `reactor_pool_exec` когда-нибудь начнут звать чаще, придётся переделывать.

2. Модуль: `src/core/reactor_pool.c` — Функции: `reactor_pool_post` (строки 260-287), `reactor_pool_post_exec` (326-356); вызываются как callback'и из `src/http3/http3_dispatch.c` (`http3_reactor_apply_response`, передаётся в `http_server_class.c:2570/2592`) и из `src/http3/http3_stream.c` (`http3_stream_release_via_request`, строки 96-97, через callback `http3_reactor_consumed_apply`) — malloc/free на каждое сообщение в hot path. Нужен freelist/кольцевой буфер.

3. Модуль: `src/http3/http3_connection.c` — Функция: `http3_build_listener_local` (строки 145-178) — повторный inet_pton/htons на каждый drain/dispatch, хотя локальный адрес слушателя не меняется. Закэшировать sockaddr_storage при создании listener'а.

4. Модуль: `src/http3/http3_static_response.c` (весь файл, 314 строк, для сравнения `src/http2/http2_static_response.c` — 1012 строк) — Функция: `h3_static_pump_entry` (строки ~113-206), лишняя копия через `zend_string_init` на строке ~184 — нет глобального лимита памяти на раздачу статики (в отличие от H2). Уточнить, осознанный пропуск или недоглядели.

5. Модуль: `src/http3/http3_stream.c` — Функция: `http3_stream_release` (строки 209-221) — O(n) проход по односвязному списку стримов на unlink; при большом числе стримов на соединение — O(n²) суммарно на закрытие. Сделать двусвязный список.

6. Модуль: `src/http3/http3_connection.c` — Функция: `http3_connection_free` (строки 995-1015) — обнуляет `s->conn` перед вызовом `http3_stream_release` (см. п.5), чтобы обойти unlink-проход. Хрупкая неявная инвариант между файлами — добавить assert в `http3_stream_release` (`src/http3/http3_stream.c:210`).

7. Модуль: `src/http3/http3_steer.c` — Функция: `http3_steer_block` (строки 36-54), вызывается транзитивно через `http3_steer_mask` из `http3_steer_encode` (98-118) и `http3_steer_decode` (120-133) — EVP_CIPHER_CTX_new/free на каждый вызов (не только минтинг CID, но и conn-map-miss). Сделать thread-local переиспользуемый контекст (EVP_CIPHER_CTX_reset).

8. Модуль: `src/http3/http3_dispatch.c` — Функция: `http3_stream_dispatch_to_worker` (строки 157-204) — **при backpressure запрос молча дропается вместо ответа клиенту.** Пороги: `H3_WORKER_SPILL_DEPTH=64` (строка 124, спилл на другой воркер) и `WORKER_INBOX_CAPACITY=1024` (`worker_inbox.c:21`, жёсткий потолок очереди). Между 64 и 1024 нет градации серьёзности — воркер с глубиной 65 и воркер с глубиной 1020 обрабатываются одинаково (просто "least busy из доступных"), никакого fail-fast раньше хардкапа. Когда `worker_inbox_post` всё же проваливается на 1024 (строка 197), код просто откатывает `refcount`/`dispatched` и возвращается — **клиент не получает ответа вообще**, зависает до QUIC PTO/RST. Нужно вместо тихого дропа отправлять явный 503 (RESET_STREAM / QPACK-ответ с Retry-After) уже на пороге хардкапа, и рассмотреть промежуточный уровень (например, при depth > ~80% от 1024 по всему пулу — сразу 503 не тратя время на скан least_busy).

9. Модуль: `src/http3/http3_callbacks.c` — Функции: `stream_reset_cb` (строки 2192-2209) и `stream_stop_sending_cb` (строки 2211-2228) — тела **побайтово идентичны** (тот же null-check, тот же единственный вызов `nghttp3_conn_shutdown_stream_read` с теми же аргументами), хотя триггерятся разными событиями ngtcp2 (RESET_STREAM vs STOP_SENDING). Вынести в общий static-хелпер.

10. Модуль: `src/http2/http2_static_response.c` — Функция: `h2_static_build_nv` (строки 748-753, запись `:status` псевдо-заголовка) — **дублирует** уже существующий хелпер `h2_nv_set_status` (`src/http2/http2_session.c:1757-1769`; форматирование 3 цифр статуса — тот же branchless-код побайтово, но `h2_static_build_nv` дополнительно клэмпит статус в диапазон 100-999 с fallback на 200, чего нет в `h2_nv_set_status`, — не полный побайтовый дубль, а его надмножество). Раздача статики решила заново написать построение `:status` вместо вызова готовой функции из `http2_session.c`. Заменить на вызов `h2_nv_set_status`, перенеся клэмп при необходимости.

11. Модуль: `src/http2/http2_static_response.c` vs `src/http3/http3_static_response.c` — публичные точки входа `h2_stream_send_static_response` (строки 798-1011) и `h3_stream_send_static_response` (строки 224-313) имеют идентичную сигнатуру и почти одинаковую структуру head-only ветки (dispose file_io → submit headers-only ответ → drain → on_done) и одинаковый набор полей state (`stream/conn/file_io/body_offset/body_length/on_done/user`). H3 корректно не дублирует построение заголовков (делегирует в `http3_stream_submit_response`, переиспользуя общий `h3_nv_push` из `http3_callbacks.c`), а вот у H2 header-billding — свой отдельный `h2_static_build_nv`, который частично дублирует `http2_session.c` (см. п.10). Общий boilerplate инициализации `state` — кандидат на вынос в разделяемый хелпер, но разные асинхронные модели (nghttp2 event-callback chain vs TrueAsync coroutine) делают полное объединение нетривиальным — низкий приоритет.

## Мелкие / некритичные

- Модуль: `src/http3/http3_listener.c` — Функция: `http3_listener_remove_connection` (строки 1265-1327) — O(n) проход `conn_list` на каждый reap + 3 прямых `zend_hash_str_del` (1298, 1305, 1317) с memcmp-дедупом, плюс делегированный цикл переменной длины по issued CID в `http3_connection_unregister_all_issued_cids` (`http3_connection.c:926`); reap редкий относительно потока пакетов.
- Дублирование callback-таблиц `src/http2/http2_session.c` vs `src/http3/http3_callbacks.c` — ожидаемо (разные C-библиотеки: nghttp2 vs nghttp3/ngtcp2), не SRP-нарушение.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ This means you can serve a REST API over HTTP/2, push real-time events over Serv
| ✅ Ready | **Compression** | gzip (zlib-ng / zlib), Brotli, zstd — response encoding + inbound decode across H1/H2/H3. Server-side preference `zstd > br > gzip`; per-codec level setters. See [docs/COMPRESSION.md](docs/COMPRESSION.md). |
| ✅ Ready | **WebSocket** | RFC 6455, upgrade from HTTP/1.1 and HTTP/2 (RFC 8441 Extended CONNECT), `wss://`, permessage-deflate (RFC 7692), full duplex, backpressure — 246/246 Autobahn conformance |
| ✅ Ready | **SSE (Server-Sent Events)** | `text/event-stream` framing (WHATWG §9.2) over H1/H2/H3 via `HttpResponse::sseStart/sseEvent/sseComment/sseRetry` |
| 📋 Planned | **gRPC** | Built on HTTP/2, unary and streaming RPC |
| ✅ Ready | **gRPC** | Over HTTP/2 **and** HTTP/3 via `addGrpcHandler()` — unary + all streaming shapes incl. full-duplex bidi (`readMessage`/`writeMessage`), `grpc-status`/`grpc-message` trailers + Trailers-Only, `grpc-timeout`, per-message gzip, grpc-web (binary, in-body `0x80` trailer frame) |

### Development Progress

Expand All @@ -57,7 +57,7 @@ HTTP/2 ████████████████████ 100%
HTTP/3 ████████████████████ 100%
WebSocket ████████████████████ 100%
SSE ████████████████████ 100%
gRPC ░░░░░░░░░░░░░░░░░░░░ 0%
gRPC ████████████████████ 100%
```

All ten ship-gates of the HTTP/3 plan (transport, TLS 1.3, request/response, streaming, lifecycle + drain, Alt-Svc, compliance smoke, fuzzing) are merged. Open items are post-ship performance follow-ups — `recvmmsg` inbound batching and Linux GSO outbound coalescing — that require upstream TrueAsync API extensions, plus optional ECN/pacing.
Expand Down Expand Up @@ -285,6 +285,32 @@ $server->addHttpHandler(function ($request, $response) {
fields are CR/LF-validated); `sseComment('')` emits a `:` heartbeat to hold the
connection open through proxy idle timeouts. The stream is never compressed.

### gRPC

Requests with an `application/grpc*` content-type route to the callable
registered via `addGrpcHandler()` — over HTTP/2 and HTTP/3 alike, next to the
regular HTTP handlers on the same listener:

```php
$server->addGrpcHandler(function ($request, $response) {
// Full-duplex: each readMessage() suspends until the next message
// arrives (returns null at end-of-stream), writeMessage() replies
// immediately — no need to wait for the request stream to finish.
while (($msg = $request->readMessage()) !== null) {
$response->writeMessage('echo:' . $msg);
}

$response->setTrailer('grpc-status', '0'); // implicit on clean return
});
```

Unary and all three streaming shapes use this one API; `grpc-status` /
`grpc-message` ride real HTTP trailers (or the in-body `0x80` frame for
grpc-web), uncaught exceptions map to `13 INTERNAL`, inbound
`grpc-encoding: gzip` messages inflate transparently, and
`writeMessage($msg, compress: true)` gzips replies. The client's deadline is
exposed via `$request->getGrpcTimeout()`.

Working examples live under [`examples/`](examples/):
[`minimal-server.php`](examples/minimal-server.php),
[`demo-server.php`](examples/demo-server.php),
Expand Down
2 changes: 2 additions & 0 deletions config.m4
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,8 @@ if test "$PHP_HTTP_SERVER" != "no"; then
src/formats/multipart_processor.c
src/http_request.c
src/http_response.c
src/grpc/grpc.c
src/grpc/grpc_call.c
src/http_sse.c
src/http_response_server_api.c
src/http_body_stream.c
Expand Down
2 changes: 2 additions & 0 deletions config.w32
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ if (PHP_TRUE_ASYNC_SERVER == "yes") {
"src\\http_server_class.c " +
"src\\http_request.c " +
"src\\http_response.c " +
"src\\grpc\\grpc.c " +
"src\\grpc\\grpc_call.c " +
"src\\http_sse.c " +
"src\\http_response_server_api.c " +
"src\\uploaded_file.c " +
Expand Down
Loading