Hello WASI HTTP server
To build an HTTP server on WASI, we implement and export the wasi:http/incoming-handler interface. Runtimes such as wasmtime serve and WASI-aware Kubernetes shims call that export to dispatch each incoming HTTP request to our component.
This tutorial shows how to export a WIT interface from Scala. The example handles GET /health with JSON.
Related examples are available at:
- outbound HTTP: wasi-http-client
- Spin deployment WASI http server: http-scala-wasm-health.
Project layout
http-health/
build.sbt
project/
plugins.sbt
build.properties
wit/
world.wit
src/main/scala/httphealth/
Server.scala
ServerHandler.scala
wit/world.wit
As always, write an WIT world for our component.
package example:http-health;
world health {
export wasi:http/incoming-handler@0.2.12;
}
Run wkg wit fetch from the project directory to download the WIT files this world depends on (into wit/deps/).
build.sbt
import org.scalajs.linker.interface.ESVersion
lazy val root = project
.in(file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
name := "http-health",
scalaVersion := "2.13.18",
libraryDependencies +=
"io.github.scala-wasm" %% "wasi4s" % "0.0.1+wasi-0.2.12",
scalaJSWitDirectory := baseDirectory.value / "wit",
scalaJSWitWorld := Some("health"),
scalaJSWitPackage := Some("httphealth"),
scalaJSUseMainModuleInitializer := false,
scalaJSLinkerConfig ~= {
_.withESFeatures(_.withUseWebAssembly(true).withESVersion(ESVersion.ES2022))
.withModuleKind(ModuleKind.WasmComponent)
},
)
Server.scala
For a WIT export, wit-bindgen-scala does not write handler code into managed sources. Instead, sbt scalaJSGenerateWitBindings (also run as part of compile) prints stub definitions in the log. Copy them into a Scala object.
[info] Export stubs for `wasi:http/incoming-handler@0.2.12` (copy into a static object and implement):
[info] @scala.scalajs.wit.annotation.WitExport(scala.scalajs.wit.annotation.WitScope("wasi", "http", "incoming-handler", "0.2.12"), "handle")
[info] def handle(@scala.scalajs.wit.annotation.WitName("request") request: IncomingRequest, @scala.scalajs.wit.annotation.WitName("response-out") responseOut: ResponseOutparam): Unit = ???
package httphealth
import io.github.scalawasm.wasi4s.wasi.http.types.{IncomingRequest, ResponseOutparam}
import scala.scalajs.wit.annotation.{WitExport, WitName, WitScope}
object Server {
@WitExport(WitScope("wasi", "http", "incoming-handler", "0.2.12"), "handle")
def handle(
@WitName("request") request: IncomingRequest,
@WitName("response-out") responseOut: ResponseOutparam): Unit =
ServerHandler.handle(request, responseOut)
}
ServerHandler.scala
package httphealth
import scala.scalajs.{wit => wm}
import scala.scalajs.wit.unsigned.UByte
import io.github.scalawasm.wasi4s.wasi.http.types._
import scala.util.control.NonFatal
object ServerHandler {
def handle(request: IncomingRequest, outParam: ResponseOutparam): Unit = {
try {
val path = request.pathWithQuery().getOrElse("/").takeWhile(_ != '?')
request.method() match {
case Method.Get if path == "/health" =>
send(outParam, 200, "application/json", """{"status":"ok"}""")
case _ =>
send(outParam, 404, "text/plain; charset=utf-8", "not found")
}
} catch {
case NonFatal(err) =>
send(outParam, 500, "text/plain; charset=utf-8", err.toString())
}
}
private def send(outParam: ResponseOutparam, status: Int,
contentType: String, responseBody: String): Unit = {
val headers = Fields.fromList(Array(
wm.Tuple2("content-type", contentType.getBytes().asInstanceOf[Array[UByte]])
)).getOrElse(Fields())
val response = OutgoingResponse(headers)
val payload = responseBody.getBytes().asInstanceOf[Array[UByte]]
(for {
_ <- response.setStatusCode(status.toShort)
body <- response.body()
_ = ResponseOutparam.set(outParam, new wm.Ok(response))
out <- body.write()
_ <- out.blockingWriteAndFlush(payload).mapErr(_ => ())
_ = out.close()
_ <- OutgoingBody.finish(body, wm.None).mapErr(_ => ())
} yield ()).getOrElse(throw new Error("failed to send response"))
}
}
Run
Link the component:
sbt fastLinkJS
Serve with Wasmtime (flags depend on your Wasmtime version; see running HTTP components):
wasmtime serve -W function-references,gc,exceptions path/to/main.wasm
In another terminal:
curl -s http://127.0.0.1:8080/health
{"status":"ok"}