losizm / little-net

The lightweight HTTP framework for Scala.

GitHub

little-net

The lightweight HTTP framework for Scala.

Maven Central

Table of Contents

HTTP Message

At the core of little-net is HttpMessage, which defines fundamental characteristics of an HTTP message. And there are HttpRequest and HttpResponse to define characteristics specific to their respective message types.

Building Request

An HttpRequest can be created using the factory method defined in its companion object.

import java.net.URI
import little.net.HttpRequest

// Specify request method and target
val req = HttpRequest("GET", new URI("http://localhost:9000/index.html"))

Or you can start with one of the factories defined in RequestMethod and use the HttpRequest builder methods to further define the request.

import little.net.RequestMethod.POST

// Specify request target with Content-Type header and message body
val req = POST("http://localhost:9000/users")
  .withHeader("Content-Type" -> "application/json")
  .withBody("""{ "id": 500, "name": "guest" }""")

Building Response

An HttpResponse can be created using the factory method defined in its companion object.

import little.net.HttpResponse

// Specify response status code and reason phrase
val res = HttpResponse(404, "Not Found")

Or you can start with one of the factories defined in ResponseStatus and use the HttpResponse builder methods to further define the response.

import little.net.ResponseStatus.Created

// Specify message body with Content-Type and Location headers
val res = Created("Resource was created.")
  .withHeader("Content-Type" -> "text/plain")
  .withHeader("Location" -> "http://localhost:9000/users/500")

Message Format

Every HTTP message consists of three main parts: a start line, zero or more headers, and an optional message body. These are represented in HttpMessage as follows:

trait HttpMessage {
  // Gets message start line.
  def startLine: String

  // Gets message headers.
  def headers: Seq[(String, String)]

  // Gets message body.
  def body: InputStream
  ...
}

Start Line

The start line in an HTTP request is referred to as the request line. It supplies the request method, target, and HTTP version.

import little.net.RequestMethod.GET

val req = GET("http://localhost:9000/users")

println(req.startLine) // GET /users HTTP/1.1
println(req.method)    // GET
println(req.target)    // http://localhost:9000/users
println(req.version)   // 1.1

Note the target in startLine includes only the path (with query string and fragment, if present). Whereas, the target method of HttpRequest returns the absolute URI.

The start line in an HTTP response is referred to as the status line. It supplies the HTTP version, response status code, and reason phrase.

import little.net.ResponseStatus.Ok

val res = Ok("Success")

println(res.startLine)    // HTTP/1.1 200 OK
println(res.version)      // 1.1
println(res.statusCode)   // 200
println(res.reasonPhrase) // OK

Headers

A header value can be retrieved by its field name, which is case-insensitive.

import little.net.RequestMethod.POST

val req = POST("http://localhost:9000/users")
  .withHeader("Content-Type" -> "application/json")
  .withBody("""{ "id": 500, "name": "guest" }""")

// Get header value ignoring case of field name
val contentType: Option[String] = req.getHeaderValue("content-type")

And there can be multiple values for any given field name.

import little.net.ResponseStatus.Ok

val res = Ok("Success").withHeaders(
  "Set-Cookie" -> "SessionId=12345",
  "Set-Cookie" -> "Limit=99"
)

// Get first value (i.e., SessionId=12345)
val cookie: Option[String] = res.getHeaderValue("Set-Cookie")

// Get all values
val cookies: Seq[String] = res.getHeaderValues("Set-Cookie")

Note: getHeaderValue() gets the first header value if there are multiple headers with the same field name.

See also Specialized Header Access and Specialized Cookie Access.

Message Body

The message body is represented as a java.io.InputStream. However, there are convenience methods to set the body as text or as a byte array.

import little.net.RequestMethod.POST

val req = POST("http://localhost:9000/users")
  .withHeader("Content-Type" -> "application/json")
  .withBody("""{ "id": 500, "name": "guest" }""") // Convert text to InputStream

And there are extension methods to InputStream available for getting the body as text or as a byte array.

import little.net.RequestMethod.POST
import little.net.auxiliary.InputStreamType

val req = POST("http://localhost:9000/users")
  .withHeader("Content-Type" -> "application/json")
  .withBody("""{ "id": 500, "name": "guest" }""")

// Read text from body, which is an InputStream
val textBody: String = req.body.getText()

In the example above, the body's InputStream is consumed. Hence, subsequent calls to getText() would return an empty string, unless the input stream is reset.

Here we read the body twice, reset the input stream, and read the body a third time.

import little.net.RequestMethod.POST
import little.net.auxiliary.InputStreamType

val req = POST("http://localhost:9000/users")
  .withHeader("Content-Type" -> "application/json")
  .withBody("""{ "id": 500, "name": "guest" }""")

assert(req.body.getText() == """{ "id": 500, "name": "guest" }""")
assert(req.body.getText() == "")

// Reset input stream
req.body.reset()

assert(req.body.getText() == """{ "id": 500, "name": "guest" }""")

Note this behavior is possible because the underlying InputStream supports reset() when the body is created from supplied text or a byte array. When explicitly setting the body to an InputStream, you must ensure the input stream supports reset() if you wish to mimic this behavior.

Specialized Header Access

In examples shown so far, access to message headers have been generalized. We've specified the header name by passing a String to a method to either set or retrieve the header value, which itself was a String.

However, the interface to HttpMessage can be extended to include specialized access to headers. The extensions are provided by the many type classes defined in little.net.headers.

import java.net.URI
import java.time.Instant
import little.net.ResponseStatus.MovedPermanently
import little.net.headers.{ ContentLength, Date, Location } // Include a few type classes

val res = MovedPermanently("Resource has moved.")
  .withContentLength(19) // Set Content-Length header
  .withDate(Instant.now()) // Set Date header
  .withLocation(new URI("http://localhost:9000/index.html")) // Set Location header

// Test header values
assert(res.contentLength == 19)
assert(res.date.isBefore(Instant.now().plusSeconds(3)))
assert(res.location == new URI("http://localhost:9000/index.html"))

// Optionally print header values
res.getContentLength.foreach(println)
res.getDate.foreach(println)
res.getLocation.foreach(println)

// Get header values in generalized form
val contentLength: Option[String] = res.getHeaderValue("Content-Length")
val date: Option[String] = res.getHeaderValue("Date")
val location: Option[String] = res.getHeaderValue("Location")

// Remove headers
val other = res.removeContentLength.removeDate.removeLocation

Specialized Cookie Access

In much the same way specialized access to headers is available, so too is the case for cookies. Specialized access is provided by type classes in little.net.cookies.

Request Cookies

In HttpRequest, cookies are stringed together in the Cookie header. You may, of course, access the cookies in their unbaked form using generalized header access. Or you can access them using the extension methods provided by RequestCookies, with each cookie represented as a PlainCookie.

import little.net.RequestMethod.GET
import little.net.cookies.{ PlainCookie, RequestCookies }

val req = GET("https://localhost:8080/messages")
  .withCookies(PlainCookie("ID", "12345"), PlainCookie("Limit", "99"))

// Print name and value of all cookies
req.cookies.foreach(cookie => println(s"${cookie.name} is ${cookie.value}"))

// Get cookie values by name
println(req.getCookieValue("ID"))    // Some(12345)
println(req.getCookieValue("Limit")) // Some(99)

// Get unbaked cookies
assert(req.getHeaderValue("Cookie").contains("ID=12345; Limit=99"))

Response Cookies

In HttpResponse, the cookies are a collection of Set-Cookie header values. Specialized access is provided by ResponseCookies, with each cookie represented as a SetCookie.

Along with name and value, SetCookie provides CookieAttributes to store additional attributes associated with the cookie, such as the path for which it is valid, whether it should be sent over secure channels only, etc.

import little.net.ResponseStatus.Ok
import little.net.cookies.{ CookieAttributes, ResponseCookies, SetCookie }

val res = Ok("Hi.\r\nHello.\r\nGood-bye.\r\n").withCookies(
  SetCookie("ID", "12345", CookieAttributes(path = Some("/"), secure = true)),
  SetCookie("Limit", "99")
)

// Print all cookies
res.cookies.foreach(println)

// Get cookie values by name
res.getCookieValue("ID").foreach(println)    // 12345
res.getCookieValue("Limit").foreach(println) // 99

// Get cookie attributes by name
res.getCookieAttributes("ID").foreach(println)    // (Path=/; Secure)
res.getCookieAttributes("Limit").foreach(println) // ()

// Get unbaked cookies
assert(res.getHeaderValue("Set-Cookie").contains("ID=12345; Path=/; Secure"))
assert(res.getHeaderValues("Set-Cookie").size == 2)

Note: Each response cookie is presented in its own Set-Cookie header, so getHeaderValue() retrieves the first cookie only.

HTTP Client

The HttpClient object can be used for sending a request and handling the response.

As demonstrated below, a POST request is submitted to the server, and the response handler prints a message in accordance to the response status.

import little.net.RequestMethod.POST
import little.net.client.HttpClient
import little.net.client.ResponseFilter._
import little.net.headers.{ ContentType, Location }

val req = POST("http://localhost:9000/users")
  .withContentType("application/json")
  .withBody("""{ "id": 500, "name": "guest" }""")

HttpClient.send(req) {
  case Successful(res)  => println("Successful")
  case Redirection(res) => println(s"Redirection: ${res.location}")
  case ClientError(res) => println(s"Client Error: ${res.statusCode}")
  case ServerError(res) => println(s"Server Error: ${res.statusCode}")
  case Informational(_) => println("Informational")
}

The client returns the value returned by the response handler. So you can process the response and return whatever information warranted.

import little.net.RequestMethod.GET
import little.net.auxiliary.InputStreamType
import little.net.client.HttpClient
import little.net.headers.Accept

val req = GET("http://localhost:8080/motd").withAccept("text/plain")

// Get message of the day or response status code
val result: Either[Int, String] = HttpClient.send(req) { res =>
    if (res.isSuccessful)
      Right(res.body.getText())
    else
      Left(res.statusCode)
}

Creating Client

The examples in the previous section use the HttpClient object as the client. Behind the scenes, this actually creates an instance of HttpClient for one-time usage.

If you plan to send multiple requests, you can create and maintain a reference to an instance, and use it as the client. With that, you also get access to methods corresponding to the standard HTTP request methods.

import little.net.RequestMethod.GET
import little.net.auxiliary.InputStreamType
import little.net.client.HttpClient
import little.net.headers.Accept

// Create and save HttpClient instance
val client = HttpClient()

def getMessageOfTheDay(): Either[Int, String] = {
  // Use saved reference to client
  client.get("http://localhost:8080/motd") { res =>
    if (res.isSuccessful)
      Right(res.body.getText())
    else
      Left(res.statusCode)
  }
}

Providing Truststore

When creating a client, you can supply the truststore used for all requests made via HTTPS.

import little.net.RequestMethod.POST
import little.net.client.HttpClient

// Create client that will use supplied truststore
val client = HttpClient(trustStore = Some(new File("/path/to/truststore")))

client.send(POST("https://localhost:3000/messages").withBody("Hello there!")) { res =>
  if (!res.isSuccessful)
    throw new Exception(s"Message not posted: ${res.statusCode}")
}

HTTP Server

little-net includes an extensible server framework.

To demonstrate, let's begin with a simple example.

import little.net.ResponseStatus.Ok
import little.net.server.HttpServer

val server = HttpServer.create(8080) { req =>
  Ok("Hello, world!")
}

This is as bare-bones as it gets. We create a server at port 8080, and, on each incoming request, we send Hello, world! back to the client. Although trite, it shows how easy it is to get going. What it doesn't show, however, are the pieces being put together to create the server. Minus imports, here's the semantic equivalent in long form.

val server = HttpServer.app().request(req => Ok("Hello, world!")).create(8080)

We'll use the remainder of this documentation to explain what goes into creating more practical applications.

Server Application

To build a server, you begin with ServerApplication. This is a mutable structure to which you apply changes to configure the server. Once the desired settings are applied, you invoke one of several methods to create the server.

You can obtain an instance of ServerApplication from the HttpServer object.

val app = HttpServer.app()

This gives you the default application as a starting point. With this in hand, you can override the location of the server log.

app.log(new File("/tmp/server.log"))

And there are peformance-related settings that can be tweaked as well.

app.poolSize(10)
app.queueSize(25)
app.readTimeout(3000)

The poolSize specifies the maximum number of requests that are processed concurrently, and queueSize sets the number of requests that are permitted to wait for processing — incoming requests that would exceed this limit are discarded.

Note queueSize is also used to configure server backlog (i.e., backlog of incoming connections), so technically there can be up to double queueSize waiting to be processed if both request queue and server backlog are filled.

The readTimeout controls how long a read from a socket blocks before it times out, whereafter 408 Request Timeout is sent to client.

Request Handlers

You define application-specific logic in instances of RequestHandler, and add them to the application.

import little.net.ResponseStatus.MethodNotAllowed
import little.net.headers.Allow

// Add handler to log request line and headers to stdout
app.request { req =>
  println(req.startLine)
  req.headers.foreach(println)
  println()

  // Return request for next handler
  Left(req)
}

// Add handler to allow GET and HEAD requests only
app.request { req =>
  if (req.method == "GET" || req.method == "HEAD")
    // Return request for next handler
    Left(req)
  else
    // Otherwise return response to end request chain
    Right(MethodNotAllowed().withAllow("GET, HEAD"))
}

An HttpRequest is passed to the RequestHandler, and the handler returns Either[HttpRequest, HttpResponse]. If the handler is unable to satisfy the request, it returns an HttpRequest so that the next handler can have its chance. Otherwise, it returns an HttpResponse, and any remaining handlers are effectively ignored.

Note the order in which handlers are applied matters. For instance, in the example above, you'd swap the order of handlers if you wanted to log GET and HEAD requests only, meaning all other requests would immediately be sent 405 Method Not Allowed and never make it to the handler that logs requests.

Also note a request handler is not restricted to returning the same request it was passed.

import little.net.headers.ContentLanguage

// Translates message body from French (Oui, oui.)
app.request { req =>
  def translate(in: InputStream): String = {
    ...
  }

  if (req.method == "POST" && req.contentLanguage.contains("fr"))
    Left(req.withBody(translate(req.body)).withContentLanguage("en"))
  else
    Left(req)
}

Filtering vs. Processing

There are two subclasses of RequestHandler reserved for instances where it's known the handler always returns the same type: RequestFilter always returns an HttpRequest, and RequestProcessor always returns an HttpResponse. These are filtering and processing, respectively.

The request logger can be rewritten as a RequestFilter.

app.request { req =>
  println(req.startLine)
  req.headers.foreach { case (name, value) => println(s"$name: $value") }
  println()

  req // Not wrapped in Left
}

And we used a RequestProcessor in our "Hello World" server, but here's one that would do something more meaningful.

import little.net.ResponseStatus.{ NotFound, Ok }

app.request { req =>
  def findFile(path: String): Option[File] = {
    ...
  }

  findFile(req.target.getPath).map(file => Ok(file)).getOrElse(NotFound())
}

Targeted Processing

A request processor can be included for a targeted path with or without a targeted request method.

import little.net.ResponseStatus.{ Forbidden, Ok }

// Match request method and exact path
app.request("GET", "/about") { req =>
  Ok("This server is powered by little-net.")
}

// Match exact path and any method
app.request("/private") { req =>
  Forbidden()
}

There are also methods in ServerApplication corresponding to the standard HTTP request methods.

import little.net.ResponseStatus.{ Created, Ok }

// Match GET requests to given path
app.get("/about") { req =>
  Ok("This server is powered by little-net.")
}

// Match POST requests to given path
app.post("/messages") { req =>
  def post(message: String): Int = {
    ...
  }

  val id = post(req.body.getText())
  Created().withLocation(new URI(s"/messages/$id"))
}

Path Parameters

Parameters can be specified in the path and their resolved values made available to the processor. When a parameter is specified as :param, it matches a single path component; whereas, *param matches the path component along with any remaining components including path separators (i.e., /).

import little.net.ResponseStatus.{ Accepted, NotFound, Ok }
import little.net.server.Implicits.HttpRequestType

// Match request method and parameterized path
app.delete("/orders/:id") { req =>
  def deleteOrder(id: Int): Boolean = {
    ...
  }

  // Get resolved parameter
  val id = req.params.getInt("id")

  if (deleteOrder(id))
    Accepted()
  else
    NotFound()
}

// Match prefixed path with any request method
app.get("/archive/*file") { req =>
  def findFile(path: String): Option[File] = {
    ...
  }

  // Get resolved parameter
  val file = req.params.getString("file")

  findFile(req.path).map(Ok(_)).getOrElse(NotFound())
}

Note there can be at most one *param, which must be specified as the the last component in the path. However, there can be multiple :param instances specified.

import little.net.ResponseStatus.Ok
import little.net.auxiliary.InputStreamType
import little.net.server.Implicits.HttpRequestType

// Match path with two parameters
app.post("/translate/:in/to/:out") { req =>
  def translator(text: String, from: String, to: String): String = {
    ...
  }

  val from = req.params.getString("in")
  val to = req.params.getString("out")

  Ok(translator(req.body.getText(), from, to))
}

Serving Static Files

You can include a specialized request handler to serve static files.

// Serve static files from given directory
app.static(new File("/path/to/public"))

This adds a request handler to serve files from the directory at /path/to/public. The files are mapped based on the request target path. For example, http://localhost:8080/images/logo.png would map to /path/to/public/images/logo.png.

Or, you can map a path prefix to a directory.

app.static("/app/main", new File("/path/to/public"))

In this case, http://localhost:8080/app/main/images/logo.png would map to /path/to/public/images/logo.png.

Aborting Response

At times, you may wish to omit a response for a particular request. On such occassions, you'd throw ResponseAborted from within the request handler.

import little.net.headers.Referer
import little.net.server.ResponseAborted

// Ignore requests originating from evil site
app.request { req =>
  if (req.referer.getHost == "www.phishing.com")
    throw ResponseAborted("Not trusted")
  req
}

Response Filters

In much the same way requests can be filtered, so too can responses. Response filtering is performed by including instances of ResponseFilter. They are applied, in order, after one of the request handlers generates a response.

app.response { res =>
  println(res.startLine)
  res.headers.foreach { case (name, value) => println(s"$name: $value") }
  println()

  // Return response for next filter
  res
}

This is pretty much the same as the request logger from earlier, only instead of HttpRequest, it consumes and produces HttpResponse.

And, similar to a request filter, the response filter is not restricted to returning the same response it consumed.

import little.net.headers.TransferEncoding

app.response { res =>
  res.withBody(new DeflaterInputStream(res.body))
    .withTransferEncoding("deflate, chunked")
}

Securing Server

The last piece of configuration is whether to secure the server using SSL/TLS. To use a secure transport, you must supply an appropriate key and certificate.

app.secure(new File("/path/to/private.key"), new File("/path/to/public.cert"))

Or, if you have them tucked away in a key store, you can supply the location.

// Supply location, password, and store type (i.e., JKS, JCEKS, PCKS12)
app.secure(new File("/path/to/keystore"), "s3cr3t", "pkcs12")

Creating Server

When the application has been configured, you're ready to create the server.

val server = app.create(8080)

If the server must bind to a particular host, you can provide the host name or IP address.

val server = app.create("192.168.0.2", 8080)

When created, an instance of HttpServer is returned, which can be used to query server details.

printf("Host: %s%n", server.host)
printf("Port: %d%n", server.port)
printf("Secure: %s%n", server.isSecure)
printf("Log: %s%n", server.log)
printf("Pool Size: %d%n", server.poolSize)
printf("Queue Size: %d%n", server.queueSize)
printf("Read Timeout: %d%n", server.readTimeout)
printf("Closed: %s%n", server.isClosed)

And, ultimately, it is used to gracefully shut down the server.

server.close() // Good-bye, world.

API Documentation

See the scaladoc for additional details.

License

little-net is licensed under the Apache License, Version 2. See LICENSE file for more information.