A lightweight HTTP/1.1 server for Scala Native. Zero dependencies beyond the Scala Native standard library. Compiles to a native binary with fast startup and low memory footprint.
import httpserver.*
import java.nio.charset.StandardCharsets
import java.util.concurrent.Executors
val handler: HttpRequest => HttpResponse = { req =>
req.path match
case "/" =>
HttpResponse(200, "Hello, world!")
case "/echo" =>
val body = new String(req.body.readAllBytes(), StandardCharsets.UTF_8)
HttpResponse(200, body)
case _ =>
HttpResponse(404, "Not Found")
}
val executor = Executors.newFixedThreadPool(100)
val server = HttpServer.start(8080, handler, executor)
// Block forever (or call server.stop() to shut down)
Thread.currentThread().join()case class HttpRequest(
method: String, // "GET", "POST", etc.
path: String, // "/foo/bar?q=1"
httpVersion: String, // "HTTP/1.1" or "HTTP/1.0"
headers: Seq[(String, String)], // header name-value pairs
body: InputStream, // request body stream
remoteAddress: Option[String] = None, // client IP
remotePort: Option[Int] = None // client port
)The body is an InputStream backed by the socket. For requests with Content-Length, it reads exactly that many bytes. For chunked requests, it decodes the chunked framing transparently. Read it like any other InputStream:
val bytes = req.body.readAllBytes()
// or stream it
val buf = new Array[Byte](8192)
var n = req.body.read(buf)
while n > 0 do
process(buf, 0, n)
n = req.body.read(buf)There are two response types:
Eager -- server computes Content-Length from a String body:
// Simple body
HttpResponse(200, "OK")
// With custom headers
HttpResponse(200, "OK", Seq("X-Custom" -> "value"))Streaming -- caller provides headers and an InputStream body:
// With Content-Length (caller must set it)
HttpResponse(200, Seq("Content-Length" -> fileSize.toString), fileInputStream)
// Without Content-Length (server uses chunked transfer encoding automatically)
HttpResponse(200, Seq.empty, myInputStream)val server = HttpServer.start(port, handler, executor)port: Int-- TCP port to listen onhandler: HttpRequest => HttpResponse-- called once per request, on an executor threadexecutor: java.util.concurrent.Executor-- thread pool for handling connections
Returns a ServerHandle with a stop() method for graceful shutdown.
Each connection is handled by one thread for its entire lifetime (including keep-alive). Choose your executor accordingly:
// Fixed pool -- predictable resource usage
val executor = Executors.newFixedThreadPool(200)
// Elastic pool -- scales up under load, reclaims idle threads
val pool = new ThreadPoolExecutor(
50, 200, 60L, TimeUnit.SECONDS,
new SynchronousQueue[Runnable]()
)
pool.allowCoreThreadTimeOut(true)- HTTP/1.1 and HTTP/1.0
- Keep-alive with idle timeout
- Chunked transfer encoding (request and response)
Expect: 100-continueHEADresponses (headers without body)OPTIONS *(server-wide)- Streaming request and response bodies via
InputStream - Automatic
Dateheader (cached, refreshed per second) - Connection stats counters (
HttpServer.printConnectionStats())
Tested with h1spec (33/33) and Http11Probe (161/161 scored tests passing).
Enforced rejections include:
- Malformed request lines, versions, methods
- Bare CR/LF in headers and chunked framing
- Invalid header names/values, control characters
- Missing/duplicate/malformed Host header
- Transfer-Encoding + Content-Length simultaneously (smuggling mitigation)
- Invalid chunk extensions and sizes
- Non-ASCII or fragment characters in request target
- URI length limit (8192 bytes)
Requires sbt and Scala Native prerequisites (LLVM/Clang).
# Debug build (fast compile, slow runtime)
sbt stressServer/nativeLink
# Release build (slow compile, optimized runtime)
SN_PROD=true sbt stressServer/nativeLink
# Run
./stress-server/target/scala-3.3.6/stress-serverTo use the server module as a library in your own Scala Native project, add it as a dependency:
lazy val myApp = project
.enablePlugins(ScalaNativePlugin)
.dependsOn(server)Apache License 2.0 -- see LICENSE.
Copyright 2026 VirtusLab Sp. z o.o.