diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/fetch/FetchRequestIntrinsic.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/fetch/FetchRequestIntrinsic.kt index d7fb6ed2b4..bcecda038d 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/fetch/FetchRequestIntrinsic.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/intrinsics/js/fetch/FetchRequestIntrinsic.kt @@ -17,6 +17,7 @@ package elide.runtime.gvm.internals.intrinsics.js.fetch import io.micronaut.http.HttpRequest import io.netty.buffer.ByteBufInputStream import org.graalvm.polyglot.Value +import org.graalvm.polyglot.proxy.ProxyExecutable import java.io.InputStream import java.nio.charset.StandardCharsets import java.util.concurrent.atomic.AtomicBoolean @@ -32,6 +33,7 @@ import elide.runtime.gvm.js.JsError import elide.runtime.interop.ReadOnlyProxyObject import elide.runtime.intrinsics.js.* import elide.vm.annotations.Polyglot +import kotlinx.serialization.json.Json /** Implements an intrinsic for the Fetch API `Request` object. */ internal class FetchRequestIntrinsic internal constructor( @@ -39,6 +41,7 @@ internal class FetchRequestIntrinsic internal constructor( targetMethod: String = FetchRequest.Defaults.DEFAULT_METHOD, requestHeaders: FetchHeaders = FetchHeadersIntrinsic.empty(), private val bodyData: ReadableStream? = null, + private val rawBodyBytes: ByteArray? = null, ) : FetchMutableRequest, ReadOnlyProxyObject { /** * Implements options for the fetch Request constructor. @@ -83,6 +86,9 @@ internal class FetchRequestIntrinsic internal constructor( private const val MEMBER_REFERRER = "referrer" private const val MEMBER_REFERRER_POLICY = "referrerPolicy" private const val MEMBER_MEMBER_URL = "url" + private const val MEMBER_TEXT = "text" + private const val MEMBER_JSON = "json" + private const val MEMBER_ARRAY_BUFFER = "arrayBuffer" private val MemberKeys = arrayOf( MEMBER_BODY, @@ -99,9 +105,14 @@ internal class FetchRequestIntrinsic internal constructor( MEMBER_REFERRER, MEMBER_REFERRER_POLICY, MEMBER_MEMBER_URL, + MEMBER_TEXT, + MEMBER_JSON, + MEMBER_ARRAY_BUFFER, ) @JvmStatic override fun forRequest(request: HttpRequest<*>): FetchMutableRequest { + val bodyInputStream = request.getBody(InputStream::class.java).orElse(null) + val bodyBytes = bodyInputStream?.readAllBytes() return FetchRequestIntrinsic( targetUrl = URLIntrinsic.URLValue.fromURL(request.uri), targetMethod = request.method.name, @@ -112,11 +123,19 @@ internal class FetchRequestIntrinsic internal constructor( } }, ), - bodyData = request.getBody(InputStream::class.java).map { ReadableStream.wrap(it) }.orElse(null), + bodyData = bodyBytes?.let { ReadableStream.wrap(it) }, + rawBodyBytes = bodyBytes, ) } @JvmStatic override fun forRequest(request: Request): FetchRequestIntrinsic { + val bodyBytes = when (val body = request.body) { + is Body.Empty -> null + is NettyBody -> ByteBufInputStream(body.unwrap()).readAllBytes() + is PrimitiveBody.StringBody -> body.unwrap().toByteArray(StandardCharsets.UTF_8) + is PrimitiveBody.Bytes -> body.unwrap() + else -> error("Unrecognized body type: ${request.body}") + } return FetchRequestIntrinsic( targetUrl = when (val url = request.url) { is JavaNetHttpUri -> URLIntrinsic.URLValue.fromString(url.absoluteString()) @@ -132,13 +151,8 @@ internal class FetchRequestIntrinsic internal constructor( }.toList(), ), - bodyData = when (val body = request.body) { - is Body.Empty -> null - is NettyBody -> ByteBufInputStream(body.unwrap()) - is PrimitiveBody.StringBody -> body.unwrap().byteInputStream(StandardCharsets.UTF_8) - is PrimitiveBody.Bytes -> body.unwrap().inputStream() - else -> error("Unrecognized body type: ${request.body}") - }?.let { ReadableStream.wrap(it) }, + bodyData = bodyBytes?.let { ReadableStream.wrap(it) }, + rawBodyBytes = bodyBytes, ) } } @@ -197,6 +211,36 @@ internal class FetchRequestIntrinsic internal constructor( return bodyData } + // Read body bytes (uses rawBodyBytes if available) + private fun readBodyBytes(): ByteArray { + bodyConsumed.set(true) + return rawBodyBytes ?: ByteArray(0) + } + + @Polyglot override fun text(): Any { + // Return a promise-like object that resolves immediately with the text + // For simplicity, we return the text directly (synchronous behavior) + // A full implementation would return a proper JS Promise + val bytes = readBodyBytes() + return String(bytes, StandardCharsets.UTF_8) + } + + @Polyglot override fun json(): Any { + // Parse the body as JSON and return + val textContent = text() as String + if (textContent.isEmpty()) { + throw JsError.typeError("Unexpected end of JSON input") + } + // Return the raw JSON string - GraalVM will handle parsing in JS context + // For a full implementation, we'd use the JS JSON.parse + return Json.parseToJsonElement(textContent) + } + + @Polyglot override fun arrayBuffer(): Any { + // Return the raw bytes as an array + return readBodyBytes() + } + override fun toString(): String { return "$method $url" } @@ -218,6 +262,9 @@ internal class FetchRequestIntrinsic internal constructor( MEMBER_REFERRER -> referrer MEMBER_REFERRER_POLICY -> referrerPolicy MEMBER_MEMBER_URL -> url + MEMBER_TEXT -> ProxyExecutable { text() } + MEMBER_JSON -> ProxyExecutable { json() } + MEMBER_ARRAY_BUFFER -> ProxyExecutable { arrayBuffer() } else -> null } } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/js/JsServerRequestExecutionInputs.kt b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/js/JsServerRequestExecutionInputs.kt index 203a1f5d10..d40d4c8c49 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/js/JsServerRequestExecutionInputs.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/gvm/internals/js/JsServerRequestExecutionInputs.kt @@ -14,12 +14,17 @@ package elide.runtime.gvm.internals.js import java.io.InputStream import java.net.URI +import java.nio.charset.StandardCharsets import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference import elide.runtime.gvm.RequestExecutionInputs +import elide.runtime.gvm.js.JsError import elide.runtime.intrinsics.js.FetchHeaders import elide.runtime.intrinsics.js.FetchRequest import elide.runtime.intrinsics.js.ReadableStream import elide.runtime.gvm.internals.intrinsics.js.url.URLIntrinsic.URLValue as URL +import elide.vm.annotations.Polyglot +import kotlinx.serialization.json.Json /** * Defines an abstract base class for JavaScript inputs based on an HTTP [Request] type, which has been made to be @@ -34,6 +39,23 @@ internal abstract class JsServerRequestExecutionInputs ( /** Internal indicator of whether the request body stream has been consumed. */ protected val consumed: AtomicBoolean = AtomicBoolean(false) + /** Cached body bytes for text/json/arrayBuffer methods. */ + private val cachedBodyBytes: AtomicReference = AtomicReference(null) + + /** Read and cache body bytes. */ + private fun readBodyBytes(): ByteArray { + val cached = cachedBodyBytes.get() + if (cached != null) return cached + if (!hasBody()) { + cachedBodyBytes.set(ByteArray(0)) + return ByteArray(0) + } + consumed.set(true) + val bytes = requestBody().readAllBytes() + cachedBodyBytes.set(bytes) + return bytes + } + /** * ## Request: Body. * @@ -164,4 +186,36 @@ internal abstract class JsServerRequestExecutionInputs ( * @return Map of HTTP request headers to their (potentially multiple) values. */ protected abstract fun requestHeaders(): Map> + + /** + * ## Request: text() + * + * Returns the request body as a text string (UTF-8 decoded). + */ + @Polyglot override fun text(): Any { + val bytes = readBodyBytes() + return String(bytes, StandardCharsets.UTF_8) + } + + /** + * ## Request: json() + * + * Returns the request body parsed as JSON. + */ + @Polyglot override fun json(): Any { + val textContent = text() as String + if (textContent.isEmpty()) { + throw JsError.typeError("Unexpected end of JSON input") + } + return Json.parseToJsonElement(textContent) + } + + /** + * ## Request: arrayBuffer() + * + * Returns the request body as a byte array. + */ + @Polyglot override fun arrayBuffer(): Any { + return readBodyBytes() + } } diff --git a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/FetchRequest.kt b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/FetchRequest.kt index 02f3c93a37..da4ca2cff5 100644 --- a/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/FetchRequest.kt +++ b/packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/js/FetchRequest.kt @@ -270,4 +270,43 @@ import elide.vm.annotations.Polyglot * See also: [MDN, Request.url](https://developer.mozilla.org/en-US/docs/Web/API/Request/url). */ @get:Polyglot public val url: String + + /** + * ## Request: text() + * + * Returns a promise that resolves with a text representation of the request body. + * + * From MDN: + * "The text() method of the Request interface reads the request body and returns it as a promise that resolves with + * a String. The response is always decoded using UTF-8." + * + * See also: [MDN, Request.text()](https://developer.mozilla.org/en-US/docs/Web/API/Request/text). + */ + @Polyglot public fun text(): Any + + /** + * ## Request: json() + * + * Returns a promise that resolves with the result of parsing the request body as JSON. + * + * From MDN: + * "The json() method of the Request interface reads the request body and returns it as a promise that resolves with + * the result of parsing the body text as JSON." + * + * See also: [MDN, Request.json()](https://developer.mozilla.org/en-US/docs/Web/API/Request/json). + */ + @Polyglot public fun json(): Any + + /** + * ## Request: arrayBuffer() + * + * Returns a promise that resolves with an ArrayBuffer representation of the request body. + * + * From MDN: + * "The arrayBuffer() method of the Request interface reads the request body and returns it as a promise that + * resolves with an ArrayBuffer." + * + * See also: [MDN, Request.arrayBuffer()](https://developer.mozilla.org/en-US/docs/Web/API/Request/arrayBuffer). + */ + @Polyglot public fun arrayBuffer(): Any }