How to set `Set-Cookie` header in HttpResponse in Akka Scala? - scala

I have the following route in my app:
val myRoute = Route { context =>
val handler = Source.single(getRequest(context))
.via(flow(server, port))
.runWith(Sink.head).flatMap { r =>
// Add cookie to response depending on certain preconditions
context.complete(r)
}
}
My problem is that I can't use the out-of-the-box setCookie method (or can I?) because I am inside a route, so I will get a type error. I thought about manually adding a header element to the HttpResponse (in the example above r), but that is quite cumbersome.
Any ideas how I can easily add the Set-Cookie header element?

setCookie Directive
A Route is just a type definition: (RequestContext) => Future[RouteResult]. Therefore you can use function composition to add a cookie to the HttpResponse coming from the downstream service.
First create a forwarder that utilizes the predefined flow:
val forwardRequest : HttpRequest => Future[HttpResponse] =
Source
.single(_)
.via(flow(server, port))
.runWith(Sink.head)
Then compose that function with getRequest and a converter from HttpResponse to RouteResult:
val queryExternalService : Route =
getRequest andThen forwardRequest andThen (_ map RouteResult.Complete)
Finally, set the cookie:
val httpCookie : HttpCookie = ??? //not specified in question
val myRoute : Route = setCookie(httpCookie)(queryExternalService)
Manual Addendum in Route
You can manually set the cookie:
val updateHeaders : (HttpHeader) => (HttpResponse) => HttpResponse =
(newHeader) =>
(httpResponse) =>
httpResponse withHeaders {
Some(httpResponse.headers.indexWhere(_.name equalsIgnoreCase newHeader.name))
.filter(_ >= 0)
.map(index => httpResponse.headers updated (index, newHeader) )
.getOrElse( httpResponse.headers +: newHeader )
}
...
.runWith(Sink.head).flatMap { response =>
context complete updateHeaders(httpCookie)(response)
}
Pure Flow
You can even avoid using Routes altogether by passing a Flow to HttpExt#bindAndHandle:
val myRoute : Flow[HttpRequest, HttpResponse, _] =
flow(server,port) map updateHeaders(httpCookie)

Related

When to materialize content of http response in akka-http?

Let's imagine proxy application based on akka-streams and akka-http which takes (as TCP server) messages in some home-grown format, makes http requests from them, asks some other http server, converts http response back to home-grown format and replies back to the client. Simpified code below:
// as Client part
val connPool = Http().cachedHostConnectionPool[CustHttpReq](someHost, somePort)
val asClientFlow = Flow[CustHttpReq]
.via (connPool)
.map (procHttpResp)
def procHttpResp (p: (Try[HttpResponse], CustHttpReq)): Future[ByteString] = {
val (rsp, src) = p
rsp match {
case Success(response: HttpResponse) =>
for (buf <- cvtToHomeGrown (response, src))
yield buf
case Failure(ex) => ...
}
}
def cvtToHomeGrown (rsp: HttpResponse): Future[ByteString] = {
rsp.entity.dataBytes.runWith (Sink.fold (ByteString.empty)(_ ++ _))
.map (cvtToHomeGrownActually) // has signature String => ByteString
}
// as Server part
val parseAndAskFlow = Flow[ByteString]
.via(Framing.delimiter(
ByteString('\n'))
.map (buf => cvtToCustHttpReq (buf))
.via (asClientFlow) // plug-in asClient part, the problem is here
val asServerConn: Source[IncomingConnection, Future[ServerBinding]] = Tcp().bind("localhost",port)
asServerConn.runForeach (conn => conn.handleWith(parseAndAskFlow)
The problem is that conn.handleWith requires Flow[ByteString,ByteString,], but http client code (rsp.entity.dataBytes...) returns Future[ByteSring], so parseAndAskFlow has Flow[ByteString,Future[ByteString],] type and I have no idea where to complete it better. I even guess it's not a good idea at all as far as all of these are streams and Await somethere will stop nice async processing, but code is not compiled.
Use mapAsync instead of map to change the type of asClientFlow to Flow[CustHttpReq, ByteString]:
val asClientFlow: Flow[CustHttpReq, ByteString] =
Flow[CustHttpReq]
.via(connPool)
.mapAsync(1)(procHttpResp)
Then parseAndAskFlow can be of type Flow[ByteString, ByteString]:
val parseAndAskFlow: Flow[ByteString, ByteString] =
Flow[ByteString]
.via(Framing.delimiter(ByteString("\n"))
.map(cvtToCustHttpReq)
.via(asClientFlow)

Appending to given formdata for http post

I am calling the following helper function from an action to make a syncronous HTTP-post with a special key-value pair appended to the URL-parameters in formdata:
def synchronousPost(url: String, formdata: Map[String, Seq[String]], timeout: Duration=Duration.create(30, TimeUnit.SECONDS)): String = {
import ExecutionContext.Implicits.global
val params = formdata.map { case (k, v) => "%s=%s".format(k, URLEncoder.encode(v.head, "UTF-8")) }.mkString("&")
val future: Future[WSResponse] = ws.url(url).
withHttpHeaders(("Content-Type", "application/x-www-form-urlencoded")).
post(params)
try {
Await.result(future, timeout)
future.value.get.get.body
} catch {
case ex: Exception =>
Logger.error(ex.toString)
ex.toString
}
}
It is called like this:
def act = Action { request =>
request.body.asFormUrlEncoded match {
case Some(formdata) =>
synchronousPost(url, formdata + ("handshake" -> List("ok"))) match {
Actually it is copy-pasted from some old gist and I am quite sure it can be rewritten in a cleaner way.
How can the line val params = ... be rewritten in a cleaner way? It seems to be low level.
The original formdata comes in from request.body.asFormUrlEncoded and I simply need to append the handshake parameter to the formdata-map and send the original request back to the sender to do the handshake.
Since formdata is a Map[String, Seq[String]], a data type for which a default WSBodyWritable is provided, you can simply use it as the request body directly:
val future: Future[WSResponse] = ws.url(url)
.withHttpHeaders(("Content-Type", "application/x-www-form-urlencoded"))
.post(formdata)
Incidentally, it's considered bad form to use Await when it's easy to make Play controllers return a Future[Result] using Action.async, e.g:
def asyncPost(url: String, formdata: Map[String, Seq[String]]): Future[String] = {
ws.url(url)
.withHttpHeaders(("Content-Type", "application/x-www-form-urlencoded"))
.post(formdata)
.map(_.body)
}
def action = Action.async { request =>
request.body.asFormUrlEncoded match {
case Some(formdata) =>
asyncPost(url, formdata + ("handshake" -> List("ok"))).map { body =>
Ok("Here is the body: " + body)
} recover {
case e =>
Logger.error(e)
InternalServerError(e.getMessage)
}
case None => Future.successful(BadRequest("No form data given"))
}
}

Handling Future[WSResponse] to find success or error state

In Scala I have a call to service in controller which is returning me Future[WSResponse]. I want to make sure service is returning valid result so send Ok(..) otherwise send BadRequest(...). I don't think I can use map. Any other suggestion?
def someWork = Action.async(parse.xml) { request =>
val result:Future[WSResponse] = someService.processData(request.body.toString())
//Need to send back Ok or BadRequest Message
}
EDIT
Solution from #alextsc is working fine. Now moving to test my existing test is failing. It is getting 400 instead of 200.
test("should post something") {
val requestBody = <value>{UUID.randomUUID}</value>
val mockResponse = mock[WSResponse]
val expectedResponse: Future[WSResponse] = Future.successful(mockResponse)
val request = FakeRequest(Helpers.POST, "/posthere").withXmlBody(requestBody)
when(mockResponse.body).thenReturn("SOME_RESPONSE")
when(someService.processData(any[String])).thenReturn(expectedResponse)
val response: Future[Result] = call(controller.someWork , request)
whenReady(response) { response =>
assert(response.header.status == 200)
}
}
You're on the right track and yes, you can use map.
Since you're using Action.async already and your service returns a future as it stands all you need to do is map that future to a Future[Result] so Play can handle it:
def someWork = Action.async(parse.xml) { request =>
someService.processData(request.body.toString()).map {
// Assuming status 200 (OK) is a valid result for you.
case resp : WSResponse if resp.getStatus == 200 => Ok(...)
case _ => BadRequest(...)
}
}
(I note that your service returns WSResponse (from the play ws java library) and not play.api.libs.ws.Response (the scala version of it), hence getStatus and not just status)

Akka Streams with Akka HTTP Server and Client

I'm trying to create an endpoint on my Akka Http Server which tells the users it's IP address using an external service (I know this can be performed way easier but I'm doing this as a challenge).
The code that doesn't make use of streams on the upper most layer is this:
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
val requestHandler: HttpRequest => Future[HttpResponse] = {
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
Http().singleRequest(HttpRequest(GET, Uri("http://checkip.amazonaws.com/"))).flatMap { response =>
response.entity.dataBytes.runFold(ByteString(""))(_ ++ _) map { string =>
HttpResponse(entity = HttpEntity(MediaTypes.`text/html`,
"<html><body><h1>" + string.utf8String + "</h1></body></html>"))
}
}
case _: HttpRequest =>
Future(HttpResponse(404, entity = "Unknown resource!"))
}
Http().bindAndHandleAsync(requestHandler, "localhost", 8080)
and it is working fine. However, as a challenge, I wanted to limit myself to only using streams (no Future's).
This is the layout I thought I'd use for this kind of an approach:
Source[Request] -> Flow[Request, Request] -> Flow[Request, Response] ->Flow[Response, Response] and to accommodate the 404 route, also Source[Request] -> Flow[Request, Response]. Now, if my Akka Stream knowledge serves me well, I need to use a Flow.fromGraph for such a thing, however, this is where I'm stuck.
In a Future I can do an easy map and flatMap for the various endpoints but in streams that would mean dividing up the Flow into multiple Flow's and I'm not quite sure how I'd do that. I thought about using UnzipWith and Options or a generic Broadcast.
Any help on this subject would be much appreciated.
I don't if this would be necessary? -- http://doc.akka.io/docs/akka-stream-and-http-experimental/2.0-M2/scala/stream-customize.html
You do not need to use Flow.fromGraph. Instead, a singular Flow that uses flatMapConcat will work:
//an outgoing connection flow
val checkIPFlow = Http().outgoingConnection("checkip.amazonaws.com")
//converts the final html String to an HttpResponse
def byteStrToResponse(byteStr : ByteString) =
HttpResponse(entity = new Default(MediaTypes.`text/html`,
byteStr.length,
Source.single(byteStr)))
val reqResponseFlow = Flow[HttpRequest].flatMapConcat[HttpResponse]( _ match {
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
Source.single(HttpRequest(GET, Uri("http://checkip.amazonaws.com/")))
.via(checkIPFlow)
.mapAsync(1)(_.entity.dataBytes.runFold(ByteString(""))(_ ++ _))
.map("<html><body><h1>" + _.utf8String + "</h1></body></html>")
.map(ByteString.apply)
.map(byteStrToResponse)
case _ =>
Source.single(HttpResponse(404, entity = "Unknown resource!"))
})
This Flow can then be used to bind to incoming requests:
Http().bindAndHandle(reqResponseFlow, "localhost", 8080)
And all without Futures...

How to convert from response 'set-cookie' header to request 'cookie' header in spray?

I'm attempting to use spray-client and spray-httpx and I'm having trouble figuring out how to convert 'set-cookie' headers from HttpResponse to a 'cookie' header that I'd like to set on an HttpRequest
val responseSetCookieHeaders = response.headers filter { _.name == "Set-Cookie" }
...
val requestCookieHeader:HttpHeader = ???
...
addHeader(requestCookieHeader) ~> sendReceive ~> { response => ??? }
I do see spray.http.HttpHeaders.Cookie, but I see no way to convert from an instance of HttpHeader to HttpCookie...
HttpHeaders.Cookie is a case class with an unapply method. So you can extract it from response with a simple function:
def getCookie(name: String): HttpHeader => Option[HttpCookie] = {
case Cookie(cookies) => cookies.find(_.name == name)
}
That's a bit more general case, but i think the solution is clear.
I would do this in the following way:
// some example response with cookie
val httpResponse = HttpResponse(headers = List(`Set-Cookie`(HttpCookie("a", "b"))))
// extracting HttpCookie
val httpCookie: List[HttpCookie] = httpResponse.headers.collect { case `Set-Cookie`(hc) => hc }
// adding to client pipeline
val pipeline = addHeader(Cookie(httpCookie)) ~> sendReceive