Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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"}