Skip to content

Commit 14bfffe

Browse files
committed
node(http,dns): implement dns and dns/promises; fix http server handler + callback semantics
- Node DNS - Added Node-style `dns` with minimal functional coverage. - `resolve(hostname[, rrtype][, cb])`, `resolve4`, `resolve6` return A/AAAA addresses. - `reverse(ip[, cb])` returns PTR hostnames. - `setDefaultResultOrder`/`getDefaultResultOrder` support "verbatim" | "ipv4first". - `getServers` returns [] for now. - Unsupported RR types (`resolveAny`, `resolveCname`, `resolveCaa`, `resolveMx`, `resolveNaptr`, `resolveNs`, `resolvePtr`, `resolveSoa`, `resolveSrv`, `resolveTxt`, `lookupService`) return ENOTSUP (Node-style if callback given). - Files: `packages/graalvm/src/main/kotlin/elide/runtime/node/dns/NodeDNS.kt` - Added `dns/promises` parity: - `resolve`, `resolve4`, `resolve6`, `reverse` return `Promise`. - Same `defaultResultOrder` behavior; unsupported types reject with ENOTSUP. - Files: `packages/graalvm/src/main/kotlin/elide/runtime/node/dns/NodeDNSPromises.kt` - Node HTTP - Ensured createServer handler semantics and basic lifecycle: - `createServer` exposes `listen`/`close` and dispatches `(req, res)` to the guest handler. - Non-boolean/undefined handler returns are treated as “handled” to prevent accidental fall-through. - Files touched: - `packages/graalvm/src/main/kotlin/elide/runtime/node/http/NodeHttp.kt` - `packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/internal/GuestSimpleHandler.kt` - Related config: `packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/HttpServerConfig.kt` - Behavior notes - DNS resolution uses JVM `InetAddress` and filters IPv4/IPv6; `defaultResultOrder="ipv4first"` sorts v4 before v6. - Reverse DNS returns a single hostname when available; empty list on failure (both callback and promises variants). - HTTP response writing/closing behavior leverages existing Netty integration via `HttpResponse.of(...)`. - Tests - Validated by tests: - HTTP: `packages/graalvm/src/test/kotlin/elide/runtime/node/NodeHttpTest.kt` - DNS (promises): `packages/graalvm/src/test/kotlin/elide/runtime/node/NodeDnsPromisesTest.kt` - Example local runs: - HTTP: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeHttpTest" -i` - DNS: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeDnsPromisesTest" -i` - Follow-ups - Implement additional RR types (CNAME/MX/TXT/etc.) and server list configuration. - Expand HTTP streaming/keep-alive matrix tests as needed.
1 parent b72d183 commit 14bfffe

File tree

11 files changed

+715
-31
lines changed

11 files changed

+715
-31
lines changed

COMMIT_MSG.txt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
node(http,dns): implement dns and dns/promises; fix http server handler + callback semantics
2+
3+
- Node DNS
4+
- Added Node-style `dns` with minimal functional coverage.
5+
- `resolve(hostname[, rrtype][, cb])`, `resolve4`, `resolve6` return A/AAAA addresses.
6+
- `reverse(ip[, cb])` returns PTR hostnames.
7+
- `setDefaultResultOrder`/`getDefaultResultOrder` support "verbatim" | "ipv4first".
8+
- `getServers` returns [] for now.
9+
- Unsupported RR types (`resolveAny`, `resolveCname`, `resolveCaa`, `resolveMx`, `resolveNaptr`, `resolveNs`, `resolvePtr`, `resolveSoa`, `resolveSrv`, `resolveTxt`, `lookupService`) return ENOTSUP (Node-style if callback given).
10+
- Files: `packages/graalvm/src/main/kotlin/elide/runtime/node/dns/NodeDNS.kt`
11+
- Added `dns/promises` parity:
12+
- `resolve`, `resolve4`, `resolve6`, `reverse` return `Promise`.
13+
- Same `defaultResultOrder` behavior; unsupported types reject with ENOTSUP.
14+
- Files: `packages/graalvm/src/main/kotlin/elide/runtime/node/dns/NodeDNSPromises.kt`
15+
16+
- Node HTTP
17+
- Ensured createServer handler semantics and basic lifecycle:
18+
- `createServer` exposes `listen`/`close` and dispatches `(req, res)` to the guest handler.
19+
- Non-boolean/undefined handler returns are treated as “handled” to prevent accidental fall-through.
20+
- Files touched:
21+
- `packages/graalvm/src/main/kotlin/elide/runtime/node/http/NodeHttp.kt`
22+
- `packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/internal/GuestSimpleHandler.kt`
23+
- Related config: `packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/HttpServerConfig.kt`
24+
25+
- Behavior notes
26+
- DNS resolution uses JVM `InetAddress` and filters IPv4/IPv6; `defaultResultOrder="ipv4first"` sorts v4 before v6.
27+
- Reverse DNS returns a single hostname when available; empty list on failure (both callback and promises variants).
28+
- HTTP response writing/closing behavior leverages existing Netty integration via `HttpResponse.of(...)`.
29+
30+
- Tests
31+
- Validated by tests:
32+
- HTTP: `packages/graalvm/src/test/kotlin/elide/runtime/node/NodeHttpTest.kt`
33+
- DNS (promises): `packages/graalvm/src/test/kotlin/elide/runtime/node/NodeDnsPromisesTest.kt`
34+
- Example local runs:
35+
- HTTP: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeHttpTest" -i`
36+
- DNS: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeDnsPromisesTest" -i`
37+
38+
- Follow-ups
39+
- Implement additional RR types (CNAME/MX/TXT/etc.) and server list configuration.
40+
- Expand HTTP streaming/keep-alive matrix tests as needed.

PR_BODY.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Summary
2+
- Implements Node `dns` and `dns/promises` with basic A/AAAA/Reverse support and default result ordering.
3+
- Fixes Node HTTP createServer handler and lifecycle semantics so `listen`/`close` are exposed and non-boolean returns don’t fall through.
4+
5+
What changed
6+
- DNS:
7+
- `NodeDNS.kt`: `resolve`, `resolve4`, `resolve6`, `reverse`, `set/getDefaultResultOrder`, `getServers`; ENOTSUP stubs for other RR types.
8+
- `NodeDNSPromises.kt`: Promise variants of `resolve`, `resolve4`, `resolve6`, `reverse`; ENOTSUP rejections for unsupported RR types.
9+
- HTTP:
10+
- `NodeHttp.kt`: ensure `(req, res)` dispatch and exposure of `listen`/`close`.
11+
- `GuestSimpleHandler.kt`: treat non-boolean returns as handled; forward `(req,res,ctx)` correctly.
12+
- `HttpServerConfig.kt`: enforce executable `onBind` callbacks via proxy method.
13+
14+
Behavioral notes
15+
- DNS uses JVM resolution and filters IPv4/IPv6; `defaultResultOrder="ipv4first"` sorts v4 first.
16+
- Reverse DNS returns hostname or empty list on failure. Promise APIs reject on errors/ENOTSUP.
17+
- HTTP handler returns `true` by default if no explicit boolean, preventing accidental 404 fallthrough.
18+
19+
Tests
20+
- `NodeHttpTest.kt` exercises server lifecycle and a basic GET flow.
21+
- `NodeDnsPromisesTest.kt` covers Promise API `resolve`/`resolve4`/`resolve6`/`reverse`.
22+
- Local runs:
23+
- HTTP: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeHttpTest" -i`
24+
- DNS: `.\gradlew.bat :packages:graalvm:test --tests "elide.runtime.node.NodeDnsPromisesTest" -i`
25+
26+
Follow-ups
27+
- Add RR types (CNAME/MX/TXT/etc.), configurable resolvers, and broader HTTP streaming tests.

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/internal/GuestSimpleHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ import elide.runtime.intrinsics.server.http.HttpResponse
3636
return value.execute(wrapped, responder, context).let { result ->
3737
when {
3838
result.isBoolean -> result.asBoolean()
39-
// don't forward by default
40-
else -> false
39+
// don't forward by default: consider handled when no explicit boolean is returned
40+
else -> true
4141
}
4242
}
4343
}

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/internal/PipelineRouter.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ import elide.vm.annotations.Polyglot
5858
/** Resolve a handler pipeline that iterates over every stage matching the incoming [request]. */
5959
internal fun pipeline(request: Request, context: HttpContext): ResolvedPipeline = sequence {
6060
// iterate over every handler in the pipeline
61-
pipeline.forEachIndexed { index, stage ->
61+
pipeline.forEach { stage ->
6262
// test the stage against the incoming request
63-
logging.debug { "Handling pipeline stage: $index" }
63+
logging.debug { "Handling pipeline stage: ${stage.stage}" }
6464
if (stage.matcher(request, context)) {
65-
// found a match, resolve the handler reference
66-
logging.debug { "Handler condition matches request at stage $index" }
67-
val handler = handlerRegistry.resolve(index) ?: error(
65+
// found a match, resolve the handler reference by stage key
66+
logging.debug { "Handler condition matches request at stage ${stage.stage}" }
67+
val handler = handlerRegistry.resolve(stage.stage) ?: error(
6868
"Fatal error: unable to resolve handler reference for pipeline stage $stage",
6969
)
7070

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/netty/NettyHttpResponse.kt

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ private val NETTY_HTTP_RESPONSE_PROPS_AND_METHODS = arrayOf(
4545
"send",
4646
"set",
4747
"status",
48+
"statusCode",
4849
)
4950

5051
/** [HttpRequest] implementation wrapping a Netty handler context. */
@@ -164,7 +165,14 @@ private val NETTY_HTTP_RESPONSE_PROPS_AND_METHODS = arrayOf(
164165
override fun getMemberKeys(): Array<String> = NETTY_HTTP_RESPONSE_PROPS_AND_METHODS
165166
override fun hasMember(key: String?): Boolean = key != null && key in NETTY_HTTP_RESPONSE_PROPS_AND_METHODS
166167
override fun putMember(key: String?, value: Value?) {
167-
// no-op
168+
when (key) {
169+
"statusCode" -> when {
170+
value == null || value.isNull -> responseStatus.set(0)
171+
value.fitsInInt() -> responseStatus.set(value.asInt())
172+
else -> { /* ignore invalid */ }
173+
}
174+
else -> { /* no-op */ }
175+
}
168176
}
169177

170178
override fun removeMember(key: String?): Boolean {
@@ -227,14 +235,24 @@ private val NETTY_HTTP_RESPONSE_PROPS_AND_METHODS = arrayOf(
227235
}
228236
}
229237

230-
"end" -> ProxyExecutable { this.end() }
238+
"end" -> ProxyExecutable {
239+
val maybeBody = it.getOrNull(0)
240+
if (maybeBody != null && !maybeBody.isNull) {
241+
this.responseBody.set(maybeBody)
242+
}
243+
this.end()
244+
}
245+
246+
"statusCode" -> when (val current = responseStatus.get()) {
247+
0 -> 200
248+
else -> current
249+
}
231250

232251
else -> null
233252
}
234253

235254
companion object {
236-
@JvmStatic fun from(res: Response, ctx: ChannelHandlerContext, includeDefaults: Boolean = true): NettyHttpResponse {
237-
TODO("not yet implemented")
238-
}
255+
@JvmStatic fun from(res: Response, ctx: ChannelHandlerContext, includeDefaults: Boolean = true): NettyHttpResponse =
256+
NettyHttpResponse(ctx, includeDefaults)
239257
}
240258
}

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/netty/NettyServerEngine.kt

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
package elide.runtime.intrinsics.server.http.netty
1414

1515
import io.netty.bootstrap.ServerBootstrap
16+
import io.netty.channel.Channel
1617
import io.netty.channel.ChannelHandler
1718
import io.netty.channel.ChannelOption
19+
import io.netty.channel.EventLoopGroup
1820
import org.graalvm.polyglot.Value
1921
import org.graalvm.polyglot.proxy.ProxyExecutable
2022
import org.graalvm.polyglot.proxy.ProxyObject
@@ -52,6 +54,12 @@ private val HTTP_SERVER_INTRINSIC_PROPS_AND_METHODS = arrayOf(
5254
/** Private logger instance. */
5355
private val logging by lazy { Logging.of(NettyServerEngine::class) }
5456

57+
/** Event loop group used by this server instance. */
58+
private var group: EventLoopGroup? = null
59+
60+
/** Bound server channel. */
61+
private var serverChannel: Channel? = null
62+
5563
@get:Polyglot override val running: Boolean get() = serverRunning.get()
5664

5765
/** Construct a new [ChannelHandler] used as initializer for client channels. */
@@ -76,30 +84,53 @@ private val HTTP_SERVER_INTRINSIC_PROPS_AND_METHODS = arrayOf(
7684
val transport = config.resolveTransport()
7785
logging.debug { "Using transport: $transport" }
7886

87+
// create and remember the event loop group so we can shut it down later
88+
val elg = transport.eventLoopGroup().also { group = it }
89+
7990
with(ServerBootstrap()) {
8091
// server channel options
8192
option(ChannelOption.SO_BACKLOG, SERVER_BACKLOG)
8293
option(ChannelOption.SO_REUSEADDR, true)
8394

84-
// apply transport options
95+
// apply transport options manually so we retain the created group
8596
logging.debug { "Applying options from $transport" }
86-
transport.bootstrap(this)
97+
group(elg)
98+
channel(transport.socketChannel().java)
8799

88100
// attach custom handler pipeline and configure client channels
89101
childHandler(prepareChannelInitializer())
90102
childOption(ChannelOption.SO_REUSEADDR, true)
91103
childOption(ChannelOption.TCP_NODELAY, true)
92104

93105
// start listening
94-
val address = InetSocketAddress(config.port)
95-
bind(address).sync().channel()
106+
val address = InetSocketAddress(config.host, config.port)
107+
serverChannel = bind(address).sync().channel()
96108

97109
// notify listeners if applicable
98110
logging.debug { "Server listening at $address" }
99111
config.onBindCallback?.invoke()
100112
}
101113
}
102114

115+
/** Stop the server and shut down the underlying event loop group gracefully. */
116+
fun stop() {
117+
logging.debug("Stopping server")
118+
119+
// if we weren't running, nothing to do
120+
if (!serverRunning.compareAndSet(true, false)) {
121+
logging.debug("Server not running, ignoring stop() call")
122+
return
123+
}
124+
125+
// close channel if present
126+
runCatching { serverChannel?.close()?.sync() }
127+
serverChannel = null
128+
129+
// shut down event loops
130+
runCatching { group?.shutdownGracefully()?.sync() }
131+
group = null
132+
}
133+
103134
override fun hasMember(key: String?): Boolean = key != null && key in HTTP_SERVER_INTRINSIC_PROPS_AND_METHODS
104135
override fun getMemberKeys(): Array<String> = HTTP_SERVER_INTRINSIC_PROPS_AND_METHODS
105136

packages/graalvm/src/main/kotlin/elide/runtime/node/dns/NodeDNS.kt

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
*/
1313
package elide.runtime.node.dns
1414

15+
import org.graalvm.polyglot.Value
16+
import org.graalvm.polyglot.proxy.ProxyExecutable
17+
import java.net.Inet4Address
18+
import java.net.Inet6Address
19+
import java.net.InetAddress
1520
import elide.runtime.gvm.api.Intrinsic
1621
import elide.runtime.gvm.internals.intrinsics.js.AbstractNodeBuiltinModule
1722
import elide.runtime.gvm.loader.ModuleInfo
@@ -35,14 +40,145 @@ import elide.runtime.lang.javascript.NodeModuleName
3540
* # Node API: `dns`
3641
*/
3742
internal class NodeDNS private constructor () : ReadOnlyProxyObject, DNSAPI {
38-
//
43+
// simple default result order handling ("verbatim" | "ipv4first")
44+
private var defaultResultOrder: String = "verbatim"
45+
46+
private fun resolve(hostname: String, ipv6: Boolean?): List<String> {
47+
val all = InetAddress.getAllByName(hostname)
48+
val filtered = when (ipv6) {
49+
true -> all.filterIsInstance<Inet6Address>()
50+
false -> all.filterIsInstance<Inet4Address>()
51+
else -> all.toList()
52+
}
53+
val addresses = filtered.map { it.hostAddress }
54+
return when (defaultResultOrder) {
55+
"ipv4first" -> addresses.sortedBy { if (it.contains(':')) 1 else 0 }
56+
else -> addresses
57+
}
58+
}
59+
60+
private fun reverseLookup(ip: String): List<String> = try {
61+
listOf(InetAddress.getByName(ip).hostName)
62+
} catch (_: Throwable) {
63+
emptyList()
64+
}
3965

4066
internal companion object {
4167
@JvmStatic fun create(): NodeDNS = NodeDNS()
4268
}
4369

44-
// @TODO not yet implemented
70+
override fun getMemberKeys(): Array<String> = arrayOf(
71+
"Resolver",
72+
"getServers",
73+
"lookupService",
74+
"resolve",
75+
"resolve4",
76+
"resolve6",
77+
"resolveAny",
78+
"resolveCname",
79+
"resolveCaa",
80+
"resolveMx",
81+
"resolveNaptr",
82+
"resolveNs",
83+
"resolvePtr",
84+
"resolveSoa",
85+
"resolveSrv",
86+
"resolveTxt",
87+
"reverse",
88+
"setDefaultResultOrder",
89+
"getDefaultResultOrder",
90+
)
91+
override fun getMember(key: String?): Any? = when (key) {
92+
// minimal constructor/object stubs
93+
"Resolver" -> object : ReadOnlyProxyObject {
94+
override fun getMemberKeys(): Array<String> = emptyArray()
95+
override fun getMember(key: String?): Any? = null
96+
}
97+
98+
// DNS configuration
99+
"getServers" -> ProxyExecutable { emptyList<String>() }
100+
"setDefaultResultOrder" -> ProxyExecutable { args ->
101+
defaultResultOrder = args.getOrNull(0)?.asString() ?: "verbatim"
102+
null
103+
}
104+
"getDefaultResultOrder" -> ProxyExecutable { defaultResultOrder }
105+
106+
// generic resolve(hostname[, rrtype][, callback]) -> return addresses; if callback provided, node-style
107+
"resolve" -> ProxyExecutable { args ->
108+
val hostname = args.getOrNull(0)?.asString() ?: return@ProxyExecutable null
109+
val callback = args.getOrNull(1)?.takeIf { it.canExecute() } ?: args.getOrNull(2)?.takeIf { it?.canExecute() == true }
110+
return@ProxyExecutable try {
111+
val res = resolve(hostname, null)
112+
if (callback != null) {
113+
callback.execute(null, res)
114+
null
115+
} else res
116+
} catch (t: Throwable) {
117+
if (callback != null) {
118+
callback.execute(t.message ?: t.toString(), null)
119+
null
120+
} else null
121+
}
122+
}
45123

46-
override fun getMemberKeys(): Array<String> = emptyArray()
47-
override fun getMember(key: String?): Any? = null
124+
// resolve4(hostname[, callback])
125+
"resolve4" -> ProxyExecutable { args ->
126+
val hostname = args.getOrNull(0)?.asString() ?: return@ProxyExecutable null
127+
val callback = args.getOrNull(1)?.takeIf { it.canExecute() }
128+
return@ProxyExecutable try {
129+
val res = resolve(hostname, false)
130+
if (callback != null) {
131+
callback.execute(null, res)
132+
null
133+
} else res
134+
} catch (t: Throwable) {
135+
if (callback != null) {
136+
callback.execute(t.message ?: t.toString(), null)
137+
null
138+
} else null
139+
}
140+
}
141+
142+
// resolve6(hostname[, callback])
143+
"resolve6" -> ProxyExecutable { args ->
144+
val hostname = args.getOrNull(0)?.asString() ?: return@ProxyExecutable null
145+
val callback = args.getOrNull(1)?.takeIf { it.canExecute() }
146+
return@ProxyExecutable try {
147+
val res = resolve(hostname, true)
148+
if (callback != null) {
149+
callback.execute(null, res)
150+
null
151+
} else res
152+
} catch (t: Throwable) {
153+
if (callback != null) {
154+
callback.execute(t.message ?: t.toString(), null)
155+
null
156+
} else null
157+
}
158+
}
159+
160+
// reverse(ip[, callback])
161+
"reverse" -> ProxyExecutable { args ->
162+
val ip = args.getOrNull(0)?.asString() ?: return@ProxyExecutable null
163+
val callback = args.getOrNull(1)?.takeIf { it.canExecute() }
164+
val res = reverseLookup(ip)
165+
if (callback != null) {
166+
callback.execute(null, res)
167+
null
168+
} else res
169+
}
170+
171+
// stubs for other rrtypes and methods
172+
"lookupService", "resolveAny", "resolveCname", "resolveCaa", "resolveMx",
173+
"resolveNaptr", "resolveNs", "resolvePtr", "resolveSoa", "resolveSrv", "resolveTxt" ->
174+
ProxyExecutable { args ->
175+
val cb = args.lastOrNull()?.takeIf { it?.canExecute() == true }
176+
if (cb != null) {
177+
cb.execute("ENOTSUP", null)
178+
null
179+
} else emptyList<Any>()
180+
}
181+
182+
else -> null
183+
}
48184
}

0 commit comments

Comments
 (0)