Akka-http logrequest not logging the request body - scala

I am using akka-http and trying to log a request on a specific path using logrequest :
path(Segment / "account") { id =>
logRequest("users/account", Logging.InfoLevel) {
post {
entity(as[Account]) { account => ???
complete(HttpResponse(StatusCodes.NoContent))
}
}
}
however on my log I see something like
HttpRequest(HttpMethod(POST),https://localhost:9009/api/users/123/account,List(Host: localhost:9009, User-Agent: akka-http/10.0.6, Timeout-Access: <function1>),HttpEntity.Chunked(application/json),HttpProtocol(HTTP/1.1))
what I am looking for is the exact request including the body (json) as it was sent by the requestor.

The "HttpEntity.Chunked(application/json)" segment of the log is the output of HttpEntity.Chunked#toString. To get the entire request body, which is implemented as a stream, you need to call HttpEntity#toStrict to convert the Chunked request entity into a Strict request entity. You can make this call in a custom route:
def logRequestEntity(route: Route, level: LogLevel)
(implicit m: Materializer, ex: ExecutionContext) = {
def requestEntityLoggingFunction(loggingAdapter: LoggingAdapter)(req: HttpRequest): Unit = {
val timeout = 900.millis
val bodyAsBytes: Future[ByteString] = req.entity.toStrict(timeout).map(_.data)
val bodyAsString: Future[String] = bodyAsBytes.map(_.utf8String)
bodyAsString.onComplete {
case Success(body) =>
val logMsg = s"$req\nRequest body: $body"
loggingAdapter.log(level, logMsg)
case Failure(t) =>
val logMsg = s"Failed to get the body for: $req"
loggingAdapter.error(t, logMsg)
}
}
DebuggingDirectives.logRequest(LoggingMagnet(requestEntityLoggingFunction(_)))(route)
}
To use the above, pass your route to it:
val loggedRoute = logRequestEntity(route, Logging.InfoLevel)

Related

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"))
}
}

First render in Play 2.3.x does not use correct language

I'm using simple middleware to update the Result
object WithLanguage extends ActionFunction[User, User] {
def invokeBlock[A](request: User[A], block: (User[A]) => Future[Result]): Future[Result] = {
if(request.cookies.get(Play.langCookieName).isEmpty){
val lang: Lang = Lang.get(request.user.language).getOrElse(play.api.i18n.Lang.preferred(request.acceptLanguages))
block(request).withLang(lang))
} else {
block(request)
}
}
}
But the first response is not rendered with the appropriate language - on refresh yes.
I can get it to work by performing a circular redirect in the case of setting a new cookie Future(Redirect(request.uri).withLang(...) but I wonder if there is a cleaner way.
This works ... It's possible to modify the request headers imperatively, but writing this way feels wrong.
object WithLanguage extends ActionFunction[User, User] {
def invokeBlock[A](request: User[A], block: (User[A]) => Future[Result]): Future[Result] = {
if(request.cookies.get(Play.langCookieName).isEmpty){
val cookies = Cookies(request.headers.get(COOKIE)).cookies + (Play.langCookieName -> Cookie(Play.langCookieName, request.user.language.code))
val updatedHeaders = new Headers {
val data: Seq[(String, Seq[String])] = (request.headers.toMap + (COOKIE -> Seq(Cookies.encode(cookies.values.toSeq)))).toSeq
}
val modifiedRequest = User(
user = request.user,
request = Request(request.copy(headers = updatedHeaders), request.body)
)
block(modifiedRequest).map(_.withLang(request.user.language)) // redundant cookie set on result
} else {
block(request)
}
}
}

RequestHeader doesn’t contain the request body at Play Framework 2.0 till now

I wonder, how to get a body from the request if in the doc:
trait RequestHeader extends AnyRef
The HTTP request header. Note that it doesn’t contain the request body yet.
That seems from very 2.0 version ..
Trying to get example of async handing the request's body. In order to log it to file.
object AccessLoggingFilter extends EssentialFilter {
def apply(inputAction: EssentialAction) = new EssentialAction { request =>
val accessLogger = Logger("access")
def apply(requestHeader: RequestHeader): Iteratee[Array[Byte], Result] = { ...
Logger.info(s"""Request:
Body = ${requestHeader.???} """)
There are some philosophical answers on SO, here for example. But I would not call it answer..
Yes, play doesn't allow to access the body of the request in the filter stage.
If you only want to log the body, you can create a new action for that and compose it.
This is an example from play 2.6
def withLogging(action: Action[AnyContent]): Action[AnyContent] = {
Action.async(action.parser) { request =>
request.body match {
case AnyContentAsJson(json) => Logger.info("JSON body was: " + Json.stringify(json))
case _ => //implement more logging of different body types
}
action(request)
}
}
def foo = withLogging(Action.async(cc.parsers.anyContent) { implicit request =>
// do your stuff
}))
If you have only json endpoints, you can write a specific action for that.

EssentialAction: How to Get the Body of a Request without Parsing It

Given the following EssentialAction...
object MyController extends Controller {
...
def HasToken(action: Token => EssentialAction) = EssentialAction { request =>
...
// this doesn't compile
val body = request.body match {
case json: JsValue => json.toString
case _ => ""
}
// calculate hash with body content here
...
}
// here is an authenticated action
def getUser(userId: Strign) = HasToken { token =>
Action(parse.json) { request =>
request.body.validate[User] match {
...
}
}
}
}
... how do I get the body of the request without parsing it?
I don't want and I don't need to parse the request body in HasToken since the body is going to be parsed in action getUser. I just need the raw content of the body to calculate a hash.
The code in HasToken doesn't compile because request is of type RequestHeader whereas I need a Request, which defines body.
Will this work for you ?
object MyController extends Controller {
// Your HasToken Action
def Authenticate(action: Token => EssentialAction) = EssentialAction { requestHeader =>
// ... execute logic to verify authenticity using requestHeader
}
// Your action to validate tampering of request body and validity of JSON
def Validate[A](action: Token => Request[A]) = Action(parse.json) { request =>
val body = request.body
body match {
case json: JsValue => json.toString
case _ => ""
}
// calculate hash with body content here
body.validate[User] match {
// ...
}
}
def getUser(userId: Strign) = Authenticate { token =>
Validate { user =>
//.... Continue
}
}
}
Authentication only uses RequestHeader
Validation uses Request body. (Bonus: Body is only parsed once)
EDIT:
Question #1: I don't want to validate the body in Validate... since I need a generic validation mechanism that could be used everywhere regardless of the content type (e.g. user, message, etc.).
How about adding another type param (so that it is made generic):
def Validate[A, B](action: Token => Request[A])(implicit reads: Reads[B]) = Action(parse.json) { request =>
// ...
}
Question #2: Furthermore, if the token validation fails, the body don't have to be processed (that's important in case of file upload, which has to be performed if and only if the validation succeeded). That's way, in my opinion, the best option would be to read the raw content of the body in Validate.
This can be easily achieved:
def Validate[A, B](action: Token => Request[A])(implicit reads: Reads[B]) = Action(parse.json) { request =>
val body = request.body
body match {
case json: JsValue => json.toString
case _ => ""
}
// calculate hash with body content here and figure out if the body is tampered
if (bodyIsNotTampered) {
body.validate[B] match {
// ...
}
} else {
// log and return Future.successful(BadRequest)
}
}
EDIT 3: Full solution:
import play.api.libs.json.{Json, JsValue, Format}
object CompilationUtils {
class Token
case class User(name: String)
implicit val UserFormat = Json.format[User]
def authenticate = new Token // authentication logic
def isTampered(body: JsValue) = {
val bodyAsStr: String = Json.stringify(body)
// calculate hash with body content here
false
}
}
object MyController extends Controller {
import CompilationUtils._
// Your HasToken Action
def Authenticate(action: Token => EssentialAction) = EssentialAction { requestHeader =>
action(authenticate)(requestHeader) // your execute logic to verify authenticity using requestHeader
}
// Your action to validate tampering of request body and validity of JSON
def Validate[A, B](request: Request[A])(implicit formatA: Format[A], formatB: Format[B]): Either[Result, B] = {
val body = request.body
val bodyAsJsValue = Json.toJson(body)
if (!isTampered(bodyAsJsValue)) {
bodyAsJsValue.validate[B].fold(
valid = res => Right(res),
invalid = err => Left(BadRequest(err.toString))
)
} else {
Left(BadRequest) // Request Tampered
}
}
def getUser(userId: String) = Authenticate { token =>
Action(parse.json) { request =>
Validate(request).fold(
badReq => badReq,
user =>
// continue...
Ok("")
)
}
}
}

TypeError Expected: Result, Actual: Future[SimpleResult]

I am working on a Scala service in Play to act as a proxy for another service. The problem I am having is that IntelliJ is giving me a type error saying that I should be returning a Future[SimpleResult] instead of a Result Object. Here is what I have:
def getProxy(proxyUrl: String) = Action { request =>
val urlSplit = proxyUrl.split('/')
urlSplit(0)
WS.url(Play.current.configuration.getString("Services.url.um") + proxyUrl).get().map { response =>
val contentType = response.header("Content-Type").getOrElse("text/json")
Ok(response.body)
}
}
How do I fix this so I can return a Result object?
Since Play WS.get() returns a Future[Response], which you're mapping to Future[Result], you need to use Action.async instead of Action.apply:
def getProxy(proxyUrl: String) = Action.async { request =>
val urlSplit = proxyUrl.split('/')
urlSplit(0)
WS.url(Play.current.configuration.getString("Services.url.um") + proxyUrl).get().map { response =>
val contentType = response.header("Content-Type").getOrElse("text/json")
Ok(response.body)
}
}