You are on page 1of 23

AKKA HTTP

INTRO

• Akka HTTP is not a framework rather a suite of libraries


• APIs for both server and client functions
• Internally uses Actors, Streams  backpressure for free
• HttpRequest, HttpResponse, HttpEntity (payload)
• Marshalling  Turning data to wire format (Json, XML)
DATA TYPES
• HttpRequest  method(Get, Post etc.), URI, headers(Seq), entity(payload), protocol
• HttpResponse  Status code(200, 404 etc.), headers(Seq), entity, protocol
• HttpEntity
• HttpEntity.Strict
• HttpEntity.Default
• HttpEntity.Chunked
• HttpEntity.IndefiniteLength
• StatusCodes -> StatusCodes.Ok, StatusCodes.NotFound etc.
• StatusCodes.custom(777, "LeetCode", "Some reason", isSuccess = true, allowsEntity = false)
foo://example.com:8042/over/there?name=ferret#nose
• ContentType \_/ \______________/\_________/ \_________/ \__/
• HttpMethods -> HttpMethods.Get, HttpMethods.Post etc. | | | | |
scheme authority path query fragment
• Uri  scheme(http, ftp etc.), path, query(parameters), fragment can be constructed using from method
• Uri(“api/myEndpoint”) creates Uri object
HTTP SERVER API(LOW LEVEL)

• Receives HttpRequest, Sends HttpResponse


• 3 ways to do
• Synchronously via a function (HttpRequest => HttpResponse)
Internally uses flow
• Asynchronously via function (HttpRequest => Future[HttpResponse])
• Asynchronously via Streams (Flow[HttpRequest, HttpResponse, _])
• Receiving requests and sending responses are done transparently
HTTP SERVER API(LOW LEVEL)
Synchronous Asynchronous

val requestHandler: HttpRequest => HttpResponse = { val asyncRequestHandler: HttpRequest => Future[HttpResponse] ={
case HttpRequest(_, _, _, _, _) => case HttpRequest(_, _, _, _, _) =>
HttpResponse() Future(HttpResponse())
} }
Http().bindAndHandleSync(requestHandler, "localhost", 8080) Http().bindAndHandleAsync(asyncRequestHandler, "localhost",
equivalent to 8080) equivalent to

Http().bind("localhost", 8080).runWith(Sink.foreach{conn=> Http().bind("localhost", 8080).runWith(Sink.foreach{conn=>


conn.handleWithSyncHandler(requestHandler)}) conn.handleWithAsyncHandler(asyncRequestHandler)})

Creating & Closing connections


• Http().bind(“localhost”, 8080)  creates a Source[Http.IncomingConnection, Future[Http.ServerBinding]
• Materialized server binding can be used to unbind(doesn’t close existing connections) and
terminate(closes existing connections too)
STREAMS BASED REQUEST HANDLER
flowBasedRequestHandler: Flow[HttpRequest, HttpResponse, _] = Flow[HttpRequest].map{
case HttpRequest(HttpMethods.GET, Uri.Path("/about"), _, _, _) =>
HttpResponse(
StatusCodes.OK,
entity = HttpEntity(
ContentTypes.`text/html(UTF-8)`,
"""
|<h1>Sample html</h1>
""".stripMargin
)
)
case request: HttpRequest =>
request.discardEntityBytes()
HttpResponse(StatusCodes.NotFound)
}
val httpsConnectionContext: HttpsConnectionContext = ConnectionContext.https(sslContext)
Http().bindAndHandle(flowBasedRequestHandler, "localhost", 8080, httpsConnectionContext)
MARSHALLING & UNMARSHALLING

• Converting higher level(object structure to and from wire format (Json, XML, YAML etc.)
• Marshaller[A, B] structure A=> Future[List[Marshalling[B]]]
• Marshaller instances need to be available implicitly for the conversion
• Unmarshaller[A, B] implements A=> Future[B]
• Spray-json support
trait CustomJsonProtocol extends DefaultJsonProtocol {
// jsonFormat based on number of parameters
implicit val orderFormat = jsonFormat3(Order)
}

• Extend the CustomJsonProtocol & SprayJsonSupport for implicit conversions incase of High-level APIs
• Use orders.toJson.prettyPrint, ordersJson.parseJson.converTo[Order] for manual conversion
HTTP SERVER API (HIGH LEVEL)
val chainedRoute: Route =
path("myEndpoint") {
get {
complete(StatusCodes.OK)
} ~ // equivalent to concat(get{}, post{})
post {
complete(StatusCodes.Forbidden)
}
}~
path("home") {
complete(
HttpEntity(ContentTypes.`text/html(UTF-8)`,
"""
|<h1>Sample html</h1>
""".stripMargin))
} // Routing tree

Http().bindAndHandle(chainedRoute, "localhost", 8080)


ROUTES
• Route is a type alias for RequestContext => Future[RouteResult]
• RequestContext wraps HttpRequest with additional information like ExecutionContext, Materializer etc.
• When a route receives a request, it can
• Complete the request
• Reject the request
• Fail the request
• Return a Future[RouteResult] by passing it to another route
• RouteResult is a composite data type of Complete(response: HttpResponse), Rejected(rejections: Seq[Rejections])
• Operations on Routes
• Transformations while sending requests to inner route
• Filtering certain requests and reject others
• Chaining, tries a second route if first one was rejected
DIRECTIVES
val route: Route = { requestContext =>
if (requestContext.request.method == HttpMethods.GET)
requestContext.complete("Received GET")
else
requestContext.complete("Received something else")
}
Both are equivalent

val routeWithDirectives =
get {
complete("Received GET")
}~
complete("Received something else")
WHAT DIRECTIVES CAN DO

• Transform the request before passing to inner route


• Filter certain requests and reject others
• Extract values from requestContext
• Chaining
• Complete the request
DIRECTIVES
• Extract variable from path (Segment in case to extract String)
path("order" / IntNumber) { id =>
complete ("Received GET request for order " + id)
}
• Directives are implemented as objects rather than methods hence can be clubbed with | operator
path("order" / IntNumber) { id =>
(get | put) {
complete(s"Received request for order $id")}
}
• As an alternative to nesting & operator can be used (extracted values are gathered up)
(path("order" / IntNumber) & get & extractMethod) { (id, m)
=>
complete(s"Received ${m.name} request for order $id")
}
(Note) an extraction directive can’t be | with non-extraction directive e.g., path(IntNumber) | get doesn’t compile
Also number of extractions and their types have to match up
val route = path("order" / IntNumber) | path("order" / DoubleNumber) // doesn't compile
val route = path("order" / IntNumber) | parameter("order".as[Int]) // ok
EXTRACTION DIRECTIVES

• Values can be extracted from path, parameters, entities etc.


• Can be directly extracted to case classes with implicit marshallers in scope
entity(implicitly[FromRequestUnmarshaller[Order]]) { order =>
complete(StatusCodes.OK)
}

entity(as[Order]]) { order =>


complete(StatusCodes.OK)
}
REJECTIONS
• In case a filtering directive like get can’t pass a request it doesn’t error rather rejects it by calling reject()
• All the rejections will be collected and will be implicitly handled by the default RejectionHandler applied by
top level code while converting Route to Flow for low-level api
• Custom RejectionHandler can be written using RejectionHandler.newBuilder().handle{case
ExampleRejection=>}.result() and it has to be made implicit
• Rejection handling can also be done by using handleRejections directive enclosing the route whose
rejections need to be handled  restricts the applicability to certain branches
• RejectionHandler extends val badRequestHandler: RejectionHandler = { rejections: Seq[Rejection] =>
(immutable.Seq[Rejection] ⇒ Option[Route]) Some(complete(StatusCodes.BadRequest))
}

val simpleRouteWithHandlers =
handleRejections(badRequestHandler) {
(path("api" / "myEndpoint") & get) {
complete(StatusCodes.OK)
}
}
EXCEPTIONS

• Exceptions like Rejections also bubble up


• ExceptionHandler extends PartialFunction[Throwable, Route]
• Custom exception handling can be done like custom rejection handling, once defined can be activated by
• Bring it into implicit scope at the top-level.
• Supply it as argument to the handleExceptions directive. implicit def myExceptionHandler: ExceptionHandler =
ExceptionHandler {
case _: ArithmeticException =>
complete(HttpResponse(StatusCodes.InternalServerError))
}

val route: Route =


path("divide") {
complete((1 / 0).toString) //Will throw ArithmeticException
} // this one takes `myExceptionHandler` implicitly
ROUTING TESTKIT

• Needed libraries akka-stream-testkit, akka-http-testkit


• Sample test
Get("/ping") ~> smallRoute ~> check {
status shouldBe StatusCodes.OK
responseAs[String] shouldEqual "PONG!"
}

• REQUEST is an expression evaluating to an HttpRequest instance


• Sample inspectors available in check block entityAs[Option[Order]], headers, rejections etc.
WEB SOCKETS
• WebSocket is a protocol that provides a bi-directional channel between browser and webserver
• Messages are either binary or text messages, are represented by the two classes BinaryMessage and
TextMessage
• The data of a message is either provided as a stream or as strict messages, represented with the
Strict subclass contains non-streamed, ByteString or String
• For sending data, you can use TextMessage(text: String) to create a strict message. Use TextMessage.
(textStream: Source[String, _]) to create a streaming message from an Akka Stream source.
• handleWebSocketMessages directive to install a WebSocket handler if a request is a WebSocket
request. Otherwise, the directive rejects the request.
• Sample code def greeter: Flow[Message, Message, Any] = Flow[Message].map {
case tm: TextMessage =>
TextMessage(Source.single("Hello ") ++ tm.textStream ++ Source.single("!"))
case bm: BinaryMessage =>
// ignore binary messages but drain content to avoid the stream being clogged
bm.dataStream.runWith(Sink.ignore)
}
val websocketRoute =
path("greeter") {
handleWebSocketMessages(greeter)
}
HTTPS CLIENT API

• Request-Level Client-Side API


for letting Akka HTTP perform all connection management. Recommended for most usages.
• Host-Level Client-Side API
for letting Akka HTTP manage a connection-pool to one specific host/port endpoint. Recommended when the user
can supply a Source[HttpRequest, _] with requests to run against a single host over multiple pooled connections.
• Connection-Level Client-Side API
for full control over when HTTP connections are opened/closed and how requests are scheduled across them. Only
recommended for particular use cases.
CONNECTION-LEVEL API
val connectionFlow: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]]
= Http().outgoingConnection(“localhost“, 8080)

def oneOffRequest(request: HttpRequest) =


Source.single(request).via(connectionFlow).runWith(Sink.head)

oneOffRequest(HttpRequest()).onComplete {
case Success(response) => println(s"Got successful response: $response")
case Failure(ex) => println(s"Sending the request failed: $ex")
}

• Even if the connectionFlow was instantiated only once above, a new connection is opened every single
time, runWith is called. Materialization and opening up a new connection is slow.
• The `outgoingConnection` API is very low-level. Use it only if you already have a Source[HttpRequest, _]
(other than Source.single) available that you want to use to run requests on a single persistent HTTP
connection.
HOST-LEVEL API
• Frees from managing individual connections  autonomously manages a configurable pool of
connections to one particular target endpoint (i.e. host/port combination).
• Ability to attach data(generally to uniquely identify requests) to requests aside from payloads
• Best for high volume, low latency requests
val poolFlow: Flow[(HttpRequest, Int), (Try[HttpResponse], Int), Http.HostConnectionPool]
= Http().cachedHostConnectionPool[Int]("www.google.com")

Source(1 to 10)
.map(i => (HttpRequest(), i))
.via(poolFlow)
.map {
case (Success(response), value) =>
// VERY IMPORTANT
response.discardEntityBytes()
s"Request $value has received response: $response"
case (Failure(ex), value) =>
s"Request $value has failed: $ex"
} .runWith(Sink.foreach[String](println))
HOST-LEVEL API

• Connection(Slot) Allocation Logic


• If there is a connection alive and currently idle, then schedule the request across this connection.
• If no connection is idle and there is still an unconnected slot, then establish a new connection.
• If all connections are already established and “loaded” with other requests then pick the connection with the
least open requests that only has requests with idempotent methods scheduled to it, if there is one.
• Otherwise apply back-pressure to the request source, i.e., stop accepting new requests.
REQUEST-LEVEL API

• Best for low-volume, low-latency requests


val responseFuture: Future[HttpResponse]
= Http().singleRequest(HttpRequest(uri = "http://www.google.com"))

Source(serverHttpRequests)
.mapAsync (10)(request => Http().singleRequest(request))
.runForeach(println)
THANK YOU

You might also like