[2.5.x] How to modify request body in filter?

1,480 views
Skip to first unread message

Chris Toomey

unread,
Aug 3, 2016, 9:26:45 PM8/3/16
to play-framework
I want to write a filter that modifies the request body, namely by decrypting it and sending on the plaintext to downstream filters/handlers. From the documentation I see I can get access to the request body via Accumulator.source[ByteString], but I don't see how to pass the modified body downstream. How do I do that? Is there an example of such a filter you can point me to?

thx,
Chris

Chris Toomey

unread,
Aug 11, 2016, 11:17:03 PM8/11/16
to play-framework
I figured it out, you have to call the next filter to get back the request body accumulator and modify that. Below's an example of a filter that duplicates each letter in the request body, e.g., "Foo!Bar" -> "FFoo!BBaarr".

Given that you have to call the next filter with the request headers to get the body, it looks like you can't modify the request headers to reflect changes you make in the request body (like Content-Length or Content-Type), which is limiting.

@Singleton
class ResponseExpander @Inject() (
implicit ec: ExecutionContext,
implicit val mat: Materializer) extends EssentialFilter {

def apply(nextFilter: EssentialAction) = new EssentialAction {

def apply(requestHeader: RequestHeader) = {
def pad(b: Byte): ByteString = {
b.toChar match {
case c if c.isLetter => ByteString(c, c)
case _ => ByteString(b)
}
}
val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader)
accumulator.mapFuture(res => {
res.body.consumeData.map(bytes => bytes.flatMap(pad))
.map(bytes => {
val entity = HttpEntity.Strict(bytes, requestHeader.headers.get(CONTENT_TYPE))
Result(res.header, entity)
})
})
}
}
}

Ashish

unread,
Aug 10, 2017, 6:34:41 AM8/10/17
to Play Framework
Hi Chris,

I want to modify the body of the request. Basically I want to change the body  which is in  XML format to JSON format but I am not able to access the body itself after upgrading play from 2.4 to 2.5.

class myCustomFilter extends EssentialFilter {
  override def apply(nextFilter: EssentialAction): EssentialAction = new EssentialAction {
    override def apply(requestHeader: RequestHeader): Accumulator[ByteString, Result] = {
// Need to change request body here and then send it to controller
}
}

Any pointers for this?

Regards,
AK

Chris Toomey

unread,
Aug 10, 2017, 5:23:25 PM8/10/17
to play-fr...@googlegroups.com
Ashish, you just have to change the code that's in my example. In my example I expanded the body by duplicating each letter, in your case you'll transform it from XML to JSON.

You'll probably need to change the Content-Type header appropriately to match. I mentioned that changing headers isn't possible, but I don't recall why or if that's really true, so you should give it a try.

Chris

--
You received this message because you are subscribed to a topic in the Google Groups "Play Framework" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/play-framework/oxajoitkX68/unsubscribe.
To unsubscribe from this group and all its topics, send an email to play-framework+unsubscribe@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/play-framework/3ae11b10-9742-4293-a351-f23adbb527fc%40googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Ashish

unread,
Aug 11, 2017, 5:51:00 AM8/11/17
to Play Framework
Hi  Chris,

Thanks for responding.

The pad function that you have written is giving me the Response body. What I need to modify is the request body. 

Basically I my API only supports XML body, but the client also send the request in JSON format also. So in such case I want to change the body to XML first and then send it to my controller. 

I tried the above example you have written and the data I get in pad function is of the response body. Are you aware of any way where I can  modify the request body and then send it to the controller?

Any pointers would be of help. 

Thanks Again!

Regards,
AK

To unsubscribe from this group and all its topics, send an email to play-framewor...@googlegroups.com.

Chris Toomey

unread,
Aug 11, 2017, 9:36:10 PM8/11/17
to Play Framework
Doh, sorry about that, not sure what I was thinking when I posted that as an example of request body modification.

Here's a corrected filter that works on the request body (the key is using the Accumulator.through() method), and following that a sample controller method that illustrates it, assuming you've got the filter hooked in. There may be a simpler way of doing it, but that's what I came up with a year ago.

package filters

import javax.inject.{Inject, Singleton}

import akka.stream.scaladsl.Flow
import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler}
import akka.stream.{Attributes, FlowShape, Materializer}
import akka.util.ByteString
import play.api.libs.streams.Accumulator
import play.api.mvc.{EssentialAction, EssentialFilter, RequestHeader, Result}

import scala.collection.mutable
import scala.concurrent.ExecutionContext


@Singleton
class RequestExpander @Inject() (

 
implicit ec: ExecutionContext,
 
implicit val mat: Materializer) extends EssentialFilter {

 
def apply(nextFilter: EssentialAction) = new EssentialAction {

   
def apply(requestHeader: RequestHeader) = {
     
def pad(b: Byte): ByteString = {
        b
.toChar match {
         
case c if c.isLetter => ByteString(c, c)
         
case _               => ByteString(b)
       
}
     
}
     
val accumulator: Accumulator[ByteString, Result] = nextFilter(requestHeader)

     
val graph = new FullBodyFilterGraphStage(bytes => bytes.flatMap(pad))
      accumulator
.through(Flow.fromGraph(graph))
   
}
 
}

 
/**
   * Internal helper class, based on
   * http://doc.akka.io/docs/akka/2.4/scala/stream/stream-cookbook.html#Calculating_the_digest_of_a_ByteString_stream
   *
   * @param filter Body filtering function
   */
  class FullBodyFilterGraphStage(filter: ByteString => ByteString) extends GraphStage[FlowShape[ByteString, ByteString]] {
   
val flow = Flow.fromFunction[ByteString, ByteString](identity)
   
override val shape = flow.shape
    val in = shape.in
   
val out = shape.out

   
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) {
     
val buffer = mutable.ArrayBuilder.make[Byte]()

      setHandler
(out, new OutHandler {
       
override def onPull(): Unit = {
          pull
(in)
       
}
     
})

      setHandler
(in, new InHandler {
       
override def onPush(): Unit = {
         
val chunk = grab(in)
         
buffer ++= chunk.toArray
          pull
(in)
       
}

       
override def onUpstreamFinish(): Unit = {
         
val bytes = buffer.result
          emit
(out, filter(ByteString(buffer.result)))
          completeStage
()
       
}
     
})
   
}
 
}
}

Controller method:

package controllers

import com.google.inject.Inject
import com.livongo.common.util.Contexts.playContext
import play.api.mvc._

import scala.concurrent.Future

class AppController @Inject() extends Controller {

 
def filterDemo = Action.async { implicit request =>
   
Future {
      request
.body.asText match {
       
case Some(body) => Ok(s"Got request body:\n$body")
       
case None => NoContent
      }
   
}
 
}
}


Ashish

unread,
Aug 17, 2017, 8:57:31 AM8/17/17
to Play Framework
Thanks Chris!

Will Try this out and let you know...

Regards,
AK
Reply all
Reply to author
Forward
0 new messages