Routing directives are handled incorrectly despite not calling reject

352 views Asked by At

Could you please explain why minor modification completely breaks my routing ?

My routing is quite simple

val myRoutes =
  pathPrefix("MainService") {
    post {
      requestInstance {
        request =>
          XmlBody {
            (command, payload) =>
              ifTrue2(command, "login") {
                complete {
                  "Return something here"    
                }
              } ~
                ifTrue2(command, "serverInfo") {
                  complete {
                    "Return something here"
                  }
                } ~
                extractSession(payload) { // OLD VERSION WAS: myAuthorization {
                  session =>
                    complete {
                      "Return something here"
                    }
                }
          }
      }

// Where custom directives look like this

def myAuthorization = entity(as[NodeSeq]).flatMap[Session :: HNil](
  getSession(_) match {
    case Some(session) => provide(session)
    case None => reject(AuthorizationFailedRejection)
  }
)

def extractSession(xmlPayload: ⇒ NodeSeq): Directive1[Session] =
  getSession(xmlPayload) match {
    case Some(session) => provide(session)
    case None => reject(AuthorizationFailedRejection)
  }

def ifTrue2(cmd : String, target : String): Directive0 =
  new Directive0 {
    def happly(func: HNil ⇒ Route) = {
      if (cmd.equalsIgnoreCase(target))
        func(HNil)
      else
        reject
    }
  }

def XmlBody = entity(as[NodeSeq]).flatMap[String :: Node :: HNil](
  parseXmlRequest(_) match {
    case Some(result) => hprovide(result)
    case None => reject(BadXmlRejection("Bad XML body"))
  }
)

def parseXmlRequest(xmlData: NodeSeq): Option[String :: Node :: HNil] = // body omitted for simplicity
def getSession(xmlRequest: NodeSeq): Option[Session] = // body omitted for simplicity

It supports two unauthenticated calls login and serverInfo. All other requests must have sessionId inside.

What I describe below happens when client makes only one login request.

Presented code works for login request when I use version with myAuthorization { }. But it doesn't work with extractSession(payload) { }. myAuthorization imiplicitly takes HttpEntity as input.

What puzzles me the most is that directives under ifTrue2 stopped working even though they have not changed. In debugger I see that IfTrue2 is called twice as expected: with ("login", "login") and ("login", "serverInfo") parameters.

Why does it work differently ? What shall I do to fix it ?

1

There are 1 answers

0
dk14 On

About bug in your new code (which causes an exception):

The entity directive takes payload from original http-request (it ignores results of hprovide - because it obviously can't understand hlists and works only with input request), your new code takes it from result of parseXmlRequest. That's the only difference.

Simply saying - now you're calling smthing like

 getSession(parseXmlRequest(request).payload) //it's now

instead of

 getSession(request.as[NodeSeq]) //it was before

So instead of <Envelope><Headers>...</><Body><function><param1>...<param1>...</></></> it passess <param1>...</>... to getSession and kills your SOAP-authorization (at least by loosing SOAP-headers and root tags) and i think throws an exception.

If you want to 'change' input request - use mapRequest instead of hprovide


About your problem with custom directives:

Spray calculates all directives, but only one appropriate complete, so you should catch exceptions inside your custom directive (one broken directive in any place may kill your whole routing):

def extractSession(xmlPayload: ⇒ NodeSeq): Directive1[Session] =
  try {
    getSession(xmlPayload) match {
      case Some(session) => provide(session)
      case None => reject(AuthorizationFailedRejection)
    }
  } catch {
    case t => reject(t)
  }
}

Example:

Actually i have ran your code with these mocks:

  def parseXmlRequest(xmlData: NodeSeq): Option[String :: Node :: HNil] = {Some(xmlData.text :: <None/> :: HNil)}

  def getSession(xmlRequest: NodeSeq): Option[Session] = { Some(Session())}

and received results as expected. But these mocks:

  def parseXmlRequest(xmlData: NodeSeq): Option[String :: Node :: HNil] = {Some(xmlData.text :: <None/> :: HNil)}

  def getSession(xmlRequest: NodeSeq): Option[Session] = { sys.error("Error")}

will always (even for login command) give you "InternalServerError" because you're not catching an exception inside directive.