feat(jsruntime/http): V8-fallback createServer bridge to native#994
Merged
Conversation
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.
516c96e to
0bae8d4
Compare
6 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
node:http'screateServerfrom the V8/JS-runtime fallback pathto a real hyper-based HTTP/1.1 server. Pre-fix the stub threw
http.createServer not supported in this environment, which crashedexpress (and would crash fastify/nestjs the same way) at module init.
op_perry_http_listen(async, binds atokio::net::TcpListener+ spawns a hyper accept loop on Perry'sshared 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, dropsthe accept-loop shutdown channel).
node:httpJS stub incrates/perry-jsruntime/src/modules.rswith a real
Serverclass + minimalIncomingMessage/ServerResponsesurface (
req.method/url/headers,res.writeHead/setHeader/write/end/statusCode).TOKIO_RUNTIMEinsideensure_runtime_initializedandwith_runtime(and
jsruntime_process_pending). Without this,JsRuntime::new'scaptured tokio handle was empty and any subsequent async op that
touched
tokio::net/tokio::spawnpanicked.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 callsres.end) worksend-to-end through the V8 path. Streaming bodies, HTTPS / HTTP/2,
WebSocket upgrades, and
req.socket/req.connectionare explicitlyout of scope — applications that need them should keep using the
native compile path through
perry-ext-http-server. See the CHANGELOGentry for the full deferred list.
Test plan
cargo build --release -p perry-runtime -p perry-stdlib -p perry-jsruntime -p perrytest-files/test_http_createserver_v8.ts(nativenode:httppath) prints200 hello— unchanged.import express from 'express'; app.get(...); app.listen(19000, cb); server.close()printslistening on 19000and exits 0 (pre-fix: thrown error at module init).fetchreturnedstatus=200 body=hi-from-middlewareand the handler loggedmiddleware called GET /.node --experimental-strip-types test-files/test_http_createserver_v8.ts(200 hello).