Skip to content

VirtusLab/picohttp

Repository files navigation

picohttp

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.

Quick start

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()

API

Request

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)

Response

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)

Starting the server

val server = HttpServer.start(port, handler, executor)
  • port: Int -- TCP port to listen on
  • handler: HttpRequest => HttpResponse -- called once per request, on an executor thread
  • executor: java.util.concurrent.Executor -- thread pool for handling connections

Returns a ServerHandle with a stop() method for graceful shutdown.

Thread pool

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)

Features

  • HTTP/1.1 and HTTP/1.0
  • Keep-alive with idle timeout
  • Chunked transfer encoding (request and response)
  • Expect: 100-continue
  • HEAD responses (headers without body)
  • OPTIONS * (server-wide)
  • Streaming request and response bodies via InputStream
  • Automatic Date header (cached, refreshed per second)
  • Connection stats counters (HttpServer.printConnectionStats())

HTTP/1.1 compliance

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)

Building

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-server

To 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)

License

Apache License 2.0 -- see LICENSE.

Copyright 2026 VirtusLab Sp. z o.o.

About

An extremely smol http server for Scala Native

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages