Srini K Srini K - 2 months ago 14
Scala Question

Spray route not working as expected. GET request also executes DELETE

Some routing is automatically executed. I think it has something to do with code in Api.scala


  • POST and PUT work fine. Weather I execute GET or DELETE, both get executed. Just wondering why?

  • During startup GET /api/v1/adsresgistrations gets executed

  • Is there a better way of organizing the routes?



RegistrationsRoute.scala

package com.admcore.api

import scala.concurrent.ExecutionContext
import spray.util.LoggingContext
import spray.json.DefaultJsonProtocol
import spray.routing._
import spray.json._
import spray.http.StatusCodes
import spray.httpx.marshalling._
import spray.httpx.SprayJsonSupport._
import akka.actor.ActorRef
import akka.pattern.ask
import scala.util.{Failure, Success}
import akka.util.Timeout
import scala.concurrent.duration._
import scala.util.Success
import scala.util.Failure
import scala.language.postfixOps
import com.admcore.service.RegistrationsService.{GetRegistrationsMessage, GetRegistrationMessage, PostRegistrationMessage, PutRegistrationMessage, DeleteRegistrationMessage}
import com.admcore.model.{Registration, RegistrationJsonProtocol}
import com.admcore.model.RegistrationJsonProtocol._
import com.admcore.Services

class RegistrationsRoutes(services: Services)(implicit ec: ExecutionContext, log: LoggingContext) extends ApiRoute(services) {

import com.admcore.api.ApiRoute._
import ApiRouteProtocol._
import com.admcore.model.RegistrationJsonProtocol._

implicit val timeout = Timeout(10 seconds)

val route: Route = {
pathPrefix("adsregistrations") {
pathEnd {
post {
entity(as[Registration]) { registration =>
withService("adsRegistrations") { service =>
val future = (service ? PostRegistrationMessage(registration)).mapTo[Registration]

onComplete(future) {
case Success(result) =>
complete(result.toString)

case Failure(e) =>
log.error(s"Error: ${e.toString}")
complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
}
}
}
} ~
put {
entity(as[Registration]) { registration =>
withService("adsRegistrations") { service =>
val future = (service ? PutRegistrationMessage(registration)).mapTo[Registration]

onComplete(future) {
case Success(result) =>
complete(result.toString)

case Failure(e) =>
log.error(s"Error: ${e.toString}")
complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
}
}
}
} ~
get {
withService("adsRegistrations") { service =>
val future = (service ? GetRegistrationsMessage()).mapTo[Vector[Registration]]

onComplete(future) {
case Success(result) =>
complete(Message(result.toString))

case Failure(e) =>
log.error(s"Error: ${e.toString}")
complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
}
}
}
} ~
pathPrefix(Segment) { (registrationId) =>
pathEnd {
delete {
withService("adsRegistrations") { service =>
val future = (service ? DeleteRegistrationMessage(registrationId)).mapTo[Registration]

onComplete(future) {
case Success(result) =>
complete(Message(result.toString))

case Failure(e) =>
log.error(s"Error: ${e.toString}")
complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
}
}
} ~
get {
withService("adsRegistrations") { service =>
val future = (service ? GetRegistrationMessage(registrationId)).mapTo[Registration]

onComplete(future) {
case Success(result) =>
complete(Message(result.toString))

case Failure(e) =>
log.error(s"Error: ${e.toString}")
complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
}
}
}
}
}
}
}
}


Registration.scala

package model

import akka.actor.{Actor, ActorLogging}
import spray.json._
import spray.json.DefaultJsonProtocol._
import spray.httpx.SprayJsonSupport
import com.mongodb.casbah.Imports._
import org.bson.types.ObjectId
import com.mongodb.DBObject
import com.mongodb.casbah.commons.{MongoDBList, MongoDBObject}

case class Registration(
system: String,
identity: String,
id: Option[String] = None)

object RegistrationJsonProtocol extends DefaultJsonProtocol {
implicit val adsRegistrationFormat = jsonFormat3(Registration)
}


RegistrationsService.scala

package com.admcore.service

import akka.actor.{Props, ActorLogging, Actor}
import spray.json._
import com.mongodb.util.JSON
import com.mongodb.casbah.Imports._
import com.admcore.DBConfiguration
import com.admcore.MongoContext
import com.admcore.model.{Registration, RegistrationDAO}


object RegistrationsService {
case class PostRegistrationMessage(registration: Registration)
case class PutRegistrationMessage(registration: Registration)
case class DeleteRegistrationMessage(registrationId: String)
case class GetRegistrationMessage(registrationId: String)
case class GetRegistrationsMessage()

def props(property: String) = Props(classOf[RegistrationsService], property)
}

class RegistrationsService(property: String) extends Actor with ActorLogging with DBConfiguration {
import RegistrationsService._

def receive = {
case PostRegistrationMessage(registration) => {
log.debug(s"POST ${registration}")

val dao = new RegistrationDAO(mongoContext.adsRegistrations)
val result = dao.insert(registration)
log.debug(s"result: $result")

sender() ! result.get
}

case PutRegistrationMessage(registration) => {
log.debug(s"PUT ${registration}")

val dao = new RegistrationDAO(mongoContext.adsRegistrations)
val result = dao.update(registration)
log.debug(s"result: $result")

sender() ! result.get
}

case DeleteRegistrationMessage(registrationId) => {
log.debug(s"DELETE ${registrationId}")

val dao = new RegistrationDAO(mongoContext.adsRegistrations)
val result = dao.delete(registrationId)
log.debug(s"result: $result")

sender() ! result.get
}

case GetRegistrationMessage(registrationId) => {
log.debug(s"GET ${registrationId}")

val dao = new RegistrationDAO(mongoContext.adsRegistrations)
val result = dao.findOne(new ObjectId(registrationId))
log.debug(s"result: $result")

sender() ! result.get
}

case GetRegistrationsMessage() => {
log.debug(s"GET all ADS registration")

val dao = new RegistrationDAO(mongoContext.adsRegistrations)
val result = dao.find()
log.debug(s"result: $result")

sender() ! result
}
}
}


Api.scala

package com.admcore.api

import spray.routing._
import akka.actor.{ActorRef, ActorLogging, Props}
import akka.io.IO
import scala.concurrent.ExecutionContext.Implicits.global
import spray.can.Http
import spray.json.DefaultJsonProtocol
import spray.util.LoggingContext
import spray.httpx.SprayJsonSupport
import spray.http.HttpHeaders.{`Access-Control-Allow-Origin`, `Access-Control-Allow-Credentials`, `Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`}
import spray.http.HttpMethods._
import spray.http.{StatusCodes, HttpOrigin, SomeOrigins}
import com.admcore.util.ConfigHolder
import com.admcore.{Services, Core, CoreActors}

trait CORSSupport extends Directives {
private val CORSHeaders = List(
`Access-Control-Allow-Methods`(GET, POST, PUT, DELETE, OPTIONS),
`Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent"),
`Access-Control-Allow-Credentials`(true)
)

def respondWithCORS(origin: String)(routes: => Route) = {
val originHeader = `Access-Control-Allow-Origin`(SomeOrigins(Seq(HttpOrigin(origin))))

respondWithHeaders(originHeader :: CORSHeaders) {
routes ~ options { complete(StatusCodes.OK) }
}
}
}

trait Api extends Directives with RouteConcatenation with CORSSupport with ConfigHolder {
this: CoreActors with Core =>

val routes =
respondWithCORS(config.getString("origin.domain")) {
pathPrefix("api" / "v1") {
new RegistrationsRoutes(services).route
}
}

val rootService = system.actorOf(ApiService.props(config.getString("hostname"), config.getInt("port"), routes))
}

object ApiService {
def props(hostname: String, port: Int, routes: Route) = Props(classOf[ApiService], hostname, port, routes)
}

class ApiService(hostname: String, port: Int, routes: Route) extends HttpServiceActor with ActorLogging {
IO(Http)(context.system) ! Http.Bind(self, hostname, port)

def receive: Receive = runRoute(routes)
}

object ApiRoute {
case class Message(message: String)

object ApiRouteProtocol extends DefaultJsonProtocol {
implicit val messageFormat = jsonFormat1(Message)
}

object ApiMessages {
val UnknownException = "Unknown exception"
val UnsupportedService = "Sorry, provided service is not supported."
}
}

abstract class ApiRoute(services: Services = Services.empty)(implicit log: LoggingContext) extends Directives with SprayJsonSupport {

import com.admcore.api.ApiRoute.{ApiMessages, Message}
import com.admcore.api.ApiRoute.ApiRouteProtocol._

def withService(id: String)(action: ActorRef => Route) = {
services.get(id) match {
case Some(provider) =>
action(provider)

case None =>
log.error("Unsupported service: $id")
complete(StatusCodes.BadRequest, Message(ApiMessages.UnsupportedService))
}
}
}

edi edi
Answer

Your routes are executed when they are being built and not at request-handling time: this applies to the GET request executed once as well as to the GET/DELETE executed simultaneously for every request. See the spray docs for an detailed explanation. To fix this behaviour, pass your future to be evaluated directly to the onComplete directive instead of assigning it to a val. This will defer evaluation of the future until the respective request is actually handled. So instead of

get {
  withService("adsRegistrations") { service =>
    val future = (service ? GetRegistrationsMessage()).mapTo[Vector[Registration]]

    onComplete(future) {
      case Success(result) =>
        complete(Message(result.toString))

      case Failure(e) =>
        log.error(s"Error: ${e.toString}")
        complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
    }
  }
}

use

get {
  withService("adsRegistrations") { service =>    
    onComplete((service ? GetRegistrationsMessage()).mapTo[Vector[Registration]]) {
      case Success(result) =>
        complete(Message(result.toString))

      case Failure(e) =>
        log.error(s"Error: ${e.toString}")
        complete(StatusCodes.InternalServerError, Message(ApiMessages.UnknownException))
    }
  }

EDIT: updated answer with possible fix

Comments