Skip to content

feat(jsruntime/http): V8-fallback createServer bridge to native#994

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a868f8ecfe47a7083
May 18, 2026
Merged

feat(jsruntime/http): V8-fallback createServer bridge to native#994
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a868f8ecfe47a7083

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • Bridges node:http's createServer from the V8/JS-runtime fallback path
    to a real hyper-based HTTP/1.1 server. Pre-fix the stub threw
    http.createServer not supported in this environment, which crashed
    express (and would crash fastify/nestjs the same way) at module init.
  • Adds four deno_core ops — op_perry_http_listen (async, binds a
    tokio::net::TcpListener + spawns a hyper accept loop on Perry's
    shared multi-thread tokio runtime), op_perry_http_accept (async,
    pops the next pending request), op_perry_http_respond (sync,
    resolves the response oneshot), op_perry_http_close (sync, drops
    the accept-loop shutdown channel).
  • Replaces the node:http JS stub in crates/perry-jsruntime/src/modules.rs
    with a real Server class + minimal IncomingMessage/ServerResponse
    surface (req.method/url/headers, res.writeHead/setHeader/
    write/end/statusCode).
  • Fixes a latent "no reactor running" panic by entering Perry's shared
    TOKIO_RUNTIME inside ensure_runtime_initialized and with_runtime
    (and jsruntime_process_pending). Without this, JsRuntime::new's
    captured tokio handle was empty and any subsequent async op that
    touched tokio::net / tokio::spawn panicked.
  • Keeps the program alive while any V8-fallback server is listening via
    a new active-server counter wired into jsruntime_has_active_handles.

Goal: enough HTTP server semantics that the prompt-cited express smoke
(app.listen(port, cb) + a single handler that calls res.end) works
end-to-end through the V8 path. Streaming bodies, HTTPS / HTTP/2,
WebSocket upgrades, and req.socket/req.connection are explicitly
out of scope — applications that need them should keep using the
native compile path through perry-ext-http-server. See the CHANGELOG
entry for the full deferred list.

Test plan

  • cargo build --release -p perry-runtime -p perry-stdlib -p perry-jsruntime -p perry
  • test-files/test_http_createserver_v8.ts (native node:http path) prints 200 hello — unchanged.
  • Express prompt smoke: import express from 'express'; app.get(...); app.listen(19000, cb); server.close() prints listening on 19000 and exits 0 (pre-fix: thrown error at module init).
  • Middleware-only express variant served a real HTTP response: external fetch returned status=200 body=hi-from-middleware and the handler logged middleware called GET /.
  • Byte-for-byte match with node --experimental-strip-types test-files/test_http_createserver_v8.ts (200 hello).

Wires the `node:http` V8 fallback stub to a real hyper-based HTTP/1.1
server via four new deno_core ops + a minimal IncomingMessage/
ServerResponse JS shim. Express's `app.listen(port, cb)` now boots
through the V8 path instead of throwing "http.createServer not
supported in this environment" at module init.

Adjacent fix: `ensure_runtime_initialized`/`with_runtime` now enter
Perry's shared TOKIO_RUNTIME so async ops touching `tokio::net` /
`tokio::spawn` don't panic with "no reactor running".

Streaming bodies / HTTPS / HTTP/2 / WebSocket upgrades remain on the
native perry-ext-http-server path; see the CHANGELOG entry for the
full deferred list.
@proggeramlug proggeramlug force-pushed the worktree-agent-a868f8ecfe47a7083 branch from 516c96e to 0bae8d4 Compare May 18, 2026 06:10
@proggeramlug proggeramlug merged commit c1c2eae into main May 18, 2026
@proggeramlug proggeramlug deleted the worktree-agent-a868f8ecfe47a7083 branch May 18, 2026 06:10
proggeramlug added a commit that referenced this pull request May 18, 2026
Post-#994 the V8-fallback `http.createServer().listen(port, cb)` smoke
was hanging in three ways. Fixes the first three (bind keepalive,
express handler polyfills, cb ordering / async fetch op); the
self-fetch deadlock inside `async () => await fetch(...)` listen
callbacks is documented as a known limitation gated on real native
async lowering.

- `last_poll_was_pending` on `JsRuntimeState` so
  `jsruntime_has_active_handles` keeps the outer event loop alive
  while deno_core has refed ops in flight, e.g. an in-progress
  `TcpListener::bind` from `op_perry_http_listen`. Pre-fix the
  outer loop exited before bind resolved and "listening" never
  printed.
- `setImmediate` / `clearImmediate` polyfill and
  `Buffer.allocUnsafeSlow` added so express's
  router + safe-buffer can run. Without these every
  `(_req, res) => res.send(...)` handler threw and the http shim
  returned 500.
- `op_perry_fetch` made async (wraps blocking ureq in
  `spawn_blocking`), and `Server.listen` now schedules the
  user-supplied listening callback via `Promise.resolve().then(cb)`
  so the JS accept loop registers its first
  `op_perry_http_accept(sid)` op before the trampoline-driven
  callback gets a chance to block the V8 thread.

Validated: `test_issue_997_v8_listen_keepalive.ts` (new) +
`test_http_createserver_v8.ts` (existing) both pass; an external
`curl http://127.0.0.1:port/ping` against a Perry-compiled
`app.get('/ping', ...)` express server returns `pong` with 200.
proggeramlug added a commit that referenced this pull request May 18, 2026
Post-#994 the V8-fallback `http.createServer().listen(port, cb)` smoke
was hanging in three ways. Fixes the first three (bind keepalive,
express handler polyfills, cb ordering / async fetch op); the
self-fetch deadlock inside `async () => await fetch(...)` listen
callbacks is documented as a known limitation gated on real native
async lowering.

- `last_poll_was_pending` on `JsRuntimeState` so
  `jsruntime_has_active_handles` keeps the outer event loop alive
  while deno_core has refed ops in flight, e.g. an in-progress
  `TcpListener::bind` from `op_perry_http_listen`. Pre-fix the
  outer loop exited before bind resolved and "listening" never
  printed.
- `setImmediate` / `clearImmediate` polyfill and
  `Buffer.allocUnsafeSlow` added so express's
  router + safe-buffer can run. Without these every
  `(_req, res) => res.send(...)` handler threw and the http shim
  returned 500.
- `op_perry_fetch` made async (wraps blocking ureq in
  `spawn_blocking`), and `Server.listen` now schedules the
  user-supplied listening callback via `Promise.resolve().then(cb)`
  so the JS accept loop registers its first
  `op_perry_http_accept(sid)` op before the trampoline-driven
  callback gets a chance to block the V8 thread.

Validated: `test_issue_997_v8_listen_keepalive.ts` (new) +
`test_http_createserver_v8.ts` (existing) both pass; an external
`curl http://127.0.0.1:port/ping` against a Perry-compiled
`app.get('/ping', ...)` express server returns `pong` with 200.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant