Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,13 +33,15 @@ 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(
targetUrl: URLIntrinsic.URLValue,
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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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())
Expand All @@ -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,
)
}
}
Expand Down Expand Up @@ -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"
}
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +39,23 @@ internal abstract class JsServerRequestExecutionInputs<Request: Any> (
/** 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<ByteArray?> = 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.
*
Expand Down Expand Up @@ -164,4 +186,36 @@ internal abstract class JsServerRequestExecutionInputs<Request: Any> (
* @return Map of HTTP request headers to their (potentially multiple) values.
*/
protected abstract fun requestHeaders(): Map<String, List<String>>

/**
* ## 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading