Scintillate
Scintillate is a lightweight HTTP client and server for sending and handling HTTP requests. It is designed primarily for Scala 3 and the optional server module provides an API for running standalone, or within a Servlet container, preferably on a Loom-based JVM.
Features
immutable API optimized for Scala 3 and lightweight concurrency with Loom
Simple and flexible request handling
HTTP server can be run standalone or wrap a servlet container
typesafe representations of HTTP request and response headers and MIME types
transparent typeclass-based request body query parameter serialization and deserialization
optional pattern-matching on requests
fast streaming without complexity
safe parameter and header access
typesafe representation of URLs
Getting Started
Here is an example of a simple HTTP request
import scintillate.*
val response = uri"http://example.com/test".query(flag = "yes", param = "7").get().as[String]
Sending an HTTP request
An HTTP request may be sent by calling one of the HTTP methods—get
, post
, put
, options
, head
, trace
,
delete
, connect
or patch
—on the Http
object. As a minimum, these methods all take a URL as their first
parameter. This may be provided as a Uri
(see below) or a String
, or any type which has a contextual
ToLocation
instance which can convert it into URL string. This may be useful for integration with alternitave
URL representations.
If the request is successful, a response will be returned synchronously as an HttpResponse
instance.
HttpResponse
provides the methods status
(the HTTP status code), headers
(a map of HTTP response headers),
and body
which will be a representation of the response body, in bytes.
The easiest way to access the body is by converting it to another type, using a contextual reader. That can be
achieved by calling as
with an appropriate type, for example,
uri"https://example.com/service".get().as[String]
or with a suitable JSON library such as Euphemism,
import euphemism.*
uri"http://example.com/file".post(content).as[Json]
Request and response bodies
The type of body
is Body
, defined as an alias for, Unit | IArray[Byte] | LazyList[IArray[Byte]]
, a union
type corresponding to the cases of an empty response, a response of known length, and a streamed response,
respectively.
This type is commonly used for both requests and responses.
Error handling
HTTP requests may fail for a variety of reasons. These will be thrown as HttpError
s only when the as
method
is invoked (an HttpResponse
is always returned from get
or post
, even in the event of a failure status).
An HttpError
contains a status
field of the HTTP status code.
Some HTTP requests will fail, but will still send a useful response body which can be read and interpreted like
any other, albeit from the HttpError
instance.
Here is an example of an HTTP error being handled:
try uri.get().as[String]
catch
case error@HttpError(HttpStatus.NotFound, _) =>
s"The page was not found. The server responded with: ${error.as[String]}"
case HttpError(_, _) =>
s"The request failed"
Launching an HTTP server
An HTTP server can be launched anywhere by calling,
HttpServer.listen:
// handler
This is a non-blocking call whose body will be executed every time a request is received. The listen
method
returns an HttpService
instance whose only method is HttpService#stop()
, which will cause the server to stop
listening for new requests.
The listen
block must return a Response
instance. A Response
may be instantiated with a single parameter
of the content to be returned. In this case, its Content-Type
and Content-Length
would be determined from
the type of the parameter, as well as the HTTP status (which is usually 200
, except in failure cases) and
how its body is sent: all at once, or streamed.
The simplest sever implementation would look something like this,
HttpServer.listen:
Response("Hello world!")
and would respond with a 200
response with the MIME type text/plain
, and the string Hello world!
for every
request, regardless of its HTTP method, parameters, body or headers.
Within the body of listen
, a Request
instance is contextually available, and may be accessed with the
request
method. For convenience, the methods param
and header
may also be used directly within a listen
block to access a parameter or HTTP header, for example:
HttpServer.listen:
val name = param("name")
val age = param("age")
Response(s"The name is $name and age is $age")
Pattern matching on Requests
Another way to work with Request
s is by pattern matching against them. Several pattern extractors are provided
for this purpose.
A very simple pattern match on a request object might look like this,
HttpServer.listen:
request match
case Path("/") => homePage
case Path("/contact") => contactUsPage
case Path(s"/products/$product") => productPage(product)
where the Path
extractor is used to match on the part of the URL after the hostname, and before the query, if
there is one.
But other extractors for matching on the HTTP method, headers and parameters also exist, and can be combined in
the same pattern using the &
combinator, like so:
HttpServer.listen:
request match
case Path("/") & RequestHeader.UserAgent(s"Mozilla/$_") => mozillaHome
case Path("/") & Post() & AcceptEncoding(lang) if lang.contains("en") => englishHome
In these examples, &
is an extractor which always matches a Request
, and “extracts” it into two copies. As
an infix extractor, both sides may then be matched with their own patterns. Of course, this can be repeated
any number of times in the same case clause.
URIs
Scintillate uses the Uri
type to represent a URI. This will always use either the http
or https
URL
scheme, and will not represent URIs containing unescaped characters. The hostname
, path
and queryString
are also available or Uri
instances.
Additionally, the HTTP methods get
, post
, and others are available to call directly on Uri
instances, as
an alternative to their equivalent methods in the Http
object. There is a one-to-one correspondence between
the methods on a Uri
instance an the Http
object, except that the first parameter of each of the Http
methods—the URI itself—is the subject of the method invocation.
Queries
A Uri
instance may include a query string, which would be written following a ?
character after the path.
The Uri#query
method may be used to append parameters to an existing Uri
. Usually these are key/value pairs
in the form key=value
, but plain strings can also be used.
There are two ways to call the query
method. Firstly, it may be invoked with variadic dynamically-named
arguments, and String
parameters, like so:
uri.query(param = "one", value = "two", flag = "three", option = "four")
the names of these parameters may be any valid Scala identifier, and do not need to be quoted.
Another variant of the query
method exists which takes a single parameter of a type that can be interpreted
as a set of parameters, based on a contextual ToQuery
instance.
Default given ToQuery
instances are provided for the primitive types, String
and Int
, and instances will
be derived for case classes composed of other types for which ToQuery
instances exist. That includes nested
case classes.
A case class will generate one query parameter for each field, named after that field, except for fields of
nested case class instances: In these cases, each field in the nested type will be prefixed with the outer field
name, separated by a .
.
For example,
case class Address(number: Int, street: String, city: String)
case class Person(name: String, address: Address)
val person = Person("Jack", Address(17, "East Street", "Birmingham"))
uri"http://example.com/person/add".query(person).get()
The process of serializing this case class instance to query parameters would send the parameters, name
,
address.number
, address.street
and address.city
.
Redirection and Missing Pages
The Redirect
and NotFound
case classes provide representations of an HTTP 301
redirect repsonse and a
404
“not found” page respectively.
Redirect
takes a single parameter, a representation of a location typically as a Uri
or a String
, but
other representations can be used provided a ToLocation
for that type is in contextual scope.