I am working on a codebase where calling my Spray API need to synchronously call a service that returns a Try which Spray need to format and return over HTTP.
My initial attempt looked like this :
// Assume myService has a run method that returns a Try[Unit]
lazy val myService = new Service()
val routes =
path("api") {
get {
tryToComplete {
myService.run()
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
tryToComplete {
myService.run(num)
}
}
}
def tryToComplete(result: => Try[Unit]): routing.Route = result match {
case Failure(t) => complete(StatusCodes.BadRequest, t.getMessage)
case Success(_) => complete("success")
}
However this caused myService.run() to be called when the application started. I am not sure why this method was called as there was no HTTP call made.
So I have two questions :
Why is my service being called as part of initialising the routes?
What is the cleanest way to handle this case? Imagine that there are a few other end points following a similar pattern. So I need to be able to handle this consistently.
Even though you have result parameter as call-by-name, it'll immediately get evaluated as you're doing
result match {
For it not to get evaluated, it has to be within the complete, ie your code should look something like (haven't tried to compile this):
def tryToComplete(result: => Try[Unit]): routing.Route = complete {
result match {
case Failure(t) => StatusCodes.BadRequest, t.getMessage
case Success(_) => "success"
}
}
The way I solved this was do do the following :
lazy val myService = new Service()
val routes =
path("api") {
get {
complete {
handleTry {
myService.run()
}
}
}
} ~
path("api" / LongNumber) { (num: Long) =>
get {
complete {
handleTry {
myService.run(num)
}
}
}
}
private def handleTry(operation: Try[_]):HttpResponse = operation match {
case Failure(t) =>
HttpResponse(status = StatusCodes.BadRequest, entity = t.getMessage)
case Success(_) =>
HttpResponse(status = StatusCodes.OK, entity = successMessage)
}
Related
I have some API Rest with CRUD operations.
Each entity is identified by using UUID.
For example for Create it is similar to this:
private val createProduct = post {
path("product" / Segment) { productUUID =>
Try(UUID.fromString(productUUID)) match {
case Failure(_) => // Bad request
case Success(validUuid) =>
entity(as[ProductData]) { productData =>
onComplete(addProduct(validUuid, productData)) {
case Success(_) => complete(StatusCodes.OK)
case Failure(ex) => // some code
}
}
}
}
}
Operations Read(GET), Update(PUT) and Delete(DELETE) are similar to POST:
first step is get the uuid
for PUT get the payload using entity(as[ProductData]) (not needed for GET and DELETE)
invoke some method that returns Future[Something]
What I would like to do is remove boilerplate like:
getting the UUID, validating it, returning Bad Request if it's not valid
create some directive/function for handling the future: if there's an exception just return 500 Internal Server Error, but in case of Success continue for processing the value (I think using provide directive).
I found this example (https://fmsbeekmans.com/blog/akka-http-2-creating-custom-directives.html):
def simplifiedOnComplete[T](future: Future[T])(timeout: FiniteDuration): Directive1[T] = {
Try(Await.result(future, Duration.Inf)) match {
case Success(result) => provide(result)
case Failure(error) => failWith(error)
}
}
I said, ok, there's a try in this example! Maybe I can change it for working with UUID instead of Future:
def getUUID[T]: Directive1[T] = {
path(Segment) { maybeuuid =>
Try(UUID.fromString(maybeuuid)) match {
case Success(result) => provide(result) // compilation error here
case Failure(error) => failWith(error)
}
}
}
The code does not compile with the error:
Type mismatch. Required: Route, found: Directive1[UUID]
I guess the problem is I've added path ...
How can I create a Directive to extract the valid uuid and return Bad Request if it's not valid?
And, is it possible to encapsulate in a custom directive the code that handles de future?
For example the routes defined at the top would be something like:
private val createProduct = post {
path ("product") {
extractUUID { validUUID =>
entity(as[ProductData]) { productData =>
futureSuccess(addProduct(validUUID, productData)) { successValue =>
// code here, for example: complete(OK)
}
}
}
}
}
You were almost there - Your code is :
def getUUID[T]: Directive1[T] = {
path(Segment) { maybeuuid =>
Try(UUID.fromString(maybeuuid)) match {
case Success(result) => provide(result) // compilation error here
case Failure(error) => failWith(error)
}
}
}
You don't need generic T since you can only have a UUID back from UUID.fromString
path(Segment) gives you a Directive. So you want to useflatMap to get a Directive back (provide returns a Directive)
So it would be something like
def getUUID: Directive1[UUID] = {
path(Segment).flatMap { maybeuuid =>
Try(UUID.fromString(maybeuuid)) match {
case Success(result) => provide(result)
case Failure(error) => failWith(error)
}
}
}
And, is it possible to encapsulate in a custom directive the code that
handles de future?
Yes, it is the same as the above. onComplete returns a Directive so you will have to flatMap.
To return BadRequest, look up the rejection handlers in the akka-http doc.
I'm sending via HTTP Post Request a Json to my Playframework backend.
In my backend I validate the Json to a Model. After that, I want to save the entries in my Model to my DB.
def parseJSON: Action[AnyContent] = Action.async {
request =>
Future {
request.body.asJson.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
itemsToDBController.saveItems(items)
Ok("Success")
case JsError(err) =>
println(err)
BadRequest("Json Parse Error")
}).getOrElse(BadRequest("Error"))
}
}
One Item consists out of several objects. To save all objects to my DB, I need to get some values. Therefore I'm using a for(..) yield(...):
def saveItems(items: MyModel) = {
items.SomeObject.map(obj => {
if (obj.value1.isDefined &&
obj.value2.isDefined ) {
val result = for (
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
) yield(value1Entry, value2Entry)
result.map({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry)
Future.successful()
}
case _ => Future.failed(new Exception("Not all entries defined"))
})
}
else {
Future.successful("Not all objects defined - skipping")
}
})
}
My problem is, after all result.map({...}) have started, my parseJSON Action returns 200 - OK. But not all relevant items get stored to my DB. It seems like after the 200 - OK everything is stopped and it doesn't even throw an error.
I don't want to use Await.result or anything blocking in my Action.
Thanks in Advance
You are starting computations by calling itemsToDBController.saveItems(items) and then immediately return result with Ok("Success"). So exception may be thrown after request if completed.
To fix this issue you need to transform result of itemsToDBController.saveItems from List[Future[T]] to Future[List[T]] with help of Future.sequence. Then call map method on returned future. Call recover on this Future to find which error is thrown:
def parseJSON: Action[AnyContent] = Action.async { request =>
request.body.asJson
.map(_.validate[MyModel] match {
case JsSuccess(items, _) =>
Future
.sequence(itemsToDBController.saveItems(items))
.map(_ => Ok("Success"))
.recover {
case e: Exception => BadRequest(e.getMessage())
}
case JsError(err) =>
println(err)
Future.successful(BadRequest("Json Parse Error"))
})
.getOrElse(Future.successful(BadRequest("Error")))
}
Update
For running all inserts in one transaction you should combine DBIOAction instead of Future. For example you rewrite checkExists(name) as:
def checkExists(name: String): DBIO[Boolean] = {
Objects.filter(obj => obj.name === name).exists
}
getOrCreateValue(exists, obj) as:
def getOrCreateValue(exists: boolean, obj: Object): DBIO[Object] = {
if (exists) {
Objects.filter(o => o.name === name).result.head
} else {
(Objects returning Objects.map(_.id) into ((o, id) => o.copy(id = Some(id)))) += obj
}
}
Now you can run it in single transaction in the following way:
def saveItems(items: MyModel) = {
val insertActions = items.SomeObject.map(obj => {
if (obj.value1.isDefined && obj.value2.isDefined) {
val result = for {
value1Exists <- value1DTO.checkExists(obj.value1.name);
value1Entry <- getOrCreateValue1(value1Exists, obj);
value2Exists <- value2DTO.checkExists(obj.value2.name);
value2Entry <- getOrCreateValue2(value1Exists, obj)
} yield (value1Entry, value2Entry)
result.flatMap({
case (value1Entry, value2Entry) => {
insertAllValue3(value1Entry, value2Entry) // This also returns instance of `DBIOAction`
}
case _ =>
DBIO.failed(new Exception("Not all entries defined"))
})
} else {
DBIO.successful("Not all objects defined - skipping")
}
})
db.run(DBIO.sequence(inserActions).transactionally)
}
For mo info how to work with DBIO actions check this official docs
Is there a way to ignore the following map/flatmap's without failed?
This is what I have:
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Future.failed(new DeleteFailedException)
// Can I return a `NotFound("...")` here instead of failed?
} else {
Future.successful(s.get)
}
}
.map{s =>
serverService.delete(s)
}.map{_ =>
Ok(Json.toJson(Map("success" -> "true")))
}
}
When I would return a NotFound("...") in the flatMap the following map would still be executed. Is there a way to ignore the following map/flatmap's?
Think so should be fine (I assumed that findByIdAndUserId returns Future[Option[_]], not an Option[_] as you answered in comment). In my approach I also removed usage of get and unnecessary map
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
request.user.flatMap(_.id).fold {
Future.successfull(NotFound("no user"))
} {userId =>
serverService.findByIdAndUserId(serverId, userId).map {
case None =>
NotFound("no server")
case Some(s) =>
serverService.delete(s)
Ok(Json.toJson(Map("success" -> "true")))
}
}
}
Instead of doing a Future.failed with an exception. you can return an Either. The good thing about either is that you can pattern match on it and then construct the appropriate http response.
def delete(serverId: UUID) = authAction.async { implicit request =>
val user = request.user.get
serverService.findByIdAndUserId(serverId, user.id.get)
.flatMap{s =>
if (s.isEmpty) {
Left(new DeleteFailedException)
} else {
Right(s.get)
}
}
.map{s => s match {
case Left(notFound) =>
// construct the Json for failure
case Right(s) =>
serverService.delete(s)
// construct json for success.
}}
}
I need to write simple web service with akka-http and reactivemongo.
Function to save data looks like this
def saveRoute(route: Route):Future[WriteResult] = {
collection.insert(route)
}
a code that calls this function looks like this
val userRoutes = {
logRequestResult("akka-http-microservice") {
path("routes") {
(post & entity(as[Route])) { route =>
Database.saveRoute(route)
}
}
}
}
I need to return result with inserted ID of Route and do this without making the thread to wait.
if try
Database.saveRoute(route).onComplete{
case Success(r) => complete(r.toString)
case Failure(e) => complete(e.getMessage)
}
It cannot compile, because it doesn't return value.
I know how to make it in dirty way, but really want to make in appropriate manner.
What should be done in this case?
Seems like I've found most efficient way to do this. It's built in onComplete directive
(path("routes" / "add") & post & entity(as[Route])) {
route =>
onComplete(routesController.addRoute(route)) {
case Success(result) => complete(StatusCodes.Created, "OK")
case Failure(ex) => complete(new ErrorResponse(StatusCodes.InternalServerError.intValue, ErrorResponse.ERROR, ex.getMessage))
}
}
Use onSuccess to handle the valid response when the future finishes and handleExceptions to handle when the future does not succeed.
val userRoutes = {
handleExceptions(mongoDbExceptionHandler) {
logRequestResult("akka-http-microservice") {
path("routes") {
(post & entity(as[Route])) { route =>
onSuccess(Database.saveRoute(route)) { result =>
complete(result)
}
}
}
}
}
}
// Something like this for whatever the exceptions you expect are
val mongoDbExceptionHandler = ExceptionHandler {
case ex: MongoDbReadException => complete(HttpResponse(InternalServerError, "No database")))
}
onSuccess:
http://doc.akka.io/docs/akka/2.4.9/scala/http/routing-dsl/directives/future-directives/onSuccess.html
handleExceptions:
http://doc.akka.io/docs/akka/2.4.9/scala/http/routing-dsl/exception-handling.html
You can map over the future and then complete the request like below.
val future = Database.saveRoute(route)
val response = future.map(_.getId).recover(_.getMessage)
complete(response)
On a side note, for handling exceptions, it is a good practice to have a ExceptionHandler and wrap it with your route. You can find example here.
You have few option i will try to put the most commonly used ones for REST API based solutions:
OnSuccess use it when you want your expectations to be bubbled and handled by expectionHandler
concat(
path("success") {
onSuccess(Future { "Ok" }) { extraction =>
complete(extraction)
}
},
path("failure") {
onSuccess(Future.failed[String](TestException)) { extraction =>
complete(extraction)
}
}
)
https://doc.akka.io/docs/akka-http/current/routing-dsl/directives/future-directives/onSuccess.html
onComplete: When you want to manually handle the exception. Try Monad wrapped.
val route =
path("divide" / IntNumber / IntNumber) { (a, b) =>
onComplete(divide(a, b)) {
case Success(value) => complete(s"The result was $value")
case Failure(ex) => complete((InternalServerError, s"An error occurred: ${ex.getMessage}"))
}
}
https://doc.akka.io/docs/akka-http/current/routing-dsl/directives/future-directives/onComplete.html
How about this, replace:
Database.saveRoute(route)
with:
complete(Database.saveRoute(route).map(_.toString).recover(_.getMessage))
When you use RequestContext you should use something like this:
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.RouteResult.{Complete, Rejected}
...
val myRoute: Route = (path("my-path") & get) { req: RequestContext =>
val futureResp: Future[HttpResponse] = ???
futureResp.map(resp => RouteResult.Complete(resp))
}
I'm new to asynchronous programming. I read this tutorial http://danielwestheide.com/blog/2013/01/09/the-neophytes-guide-to-scala-part-8-welcome-to-the-future.html and was surprised by how effortless I can incorporate Future into the program. However, when I was using Future with Routing, the return type is kind of wrong.
get {
optionalCookie("commToken") {
case Some(commCookie) =>
val response = (MTurkerProgressActor ? Register).mapTo[..].map({...})
val result = Await.result(response, 5 seconds)
setCookie(HttpCookie("commToken", content = result._2.mturker.get.commToken)) {
complete(result._1, result._2.mturker.get)
}
case None => // ...
}
}
I really don't want to use Await (what's the point of asynchronous if I just block the thread and wait for 5 seconds?). I tried to use for-comprehension or flatMap and place the setCookie and complete actions inside, but the return type is unacceptable to Spray. For-comprehension returns "Unit", and flatMap returns a Future.
Since I need to set up this cookie, I need the data inside. Is Await the solution? Or is there a smatter way?
You can use the onSuccess directive:
get {
optionalCookie("commToken") { cookie =>
//....
val response = (MTurkerProgressActor ? Register).mapTo[..].map({...})
onSuccess(response) {
case (result, mTurkerResponse) =>
setCookie(HttpCookie("commToken", content = mTurkerResponse.mturker.get.commToken)) {
complete(result, mturkerResponse.mturker.get)
}
}
}
There's also onFailure and onComplete (for which you have to match on Success and Failure) See http://spray.io/documentation/1.2.1/spray-routing/future-directives/onComplete/
Also, instead of using get directly it's much more idiomatic to use map (I assume the mturker is an Option or something similar):
case (result, mTurkerResponse) =>
mTurkerResponse.mturker.map { mt =>
setCookie(HttpCookie("commToken", content = mt.commToken)) {
complete(result, mt)
}
}
You can also make a custom directive using this code -
case class ExceptionRejection(ex: Throwable) extends Rejection
protected def futureDirective[T](x: Future[T],
exceptionHandler: (Throwable) => Rejection = ExceptionRejection(_)) =
new Directive1[T] {
override def happly(f: (::[T, HNil]) => Route): Route = { ctx =>
x
.map(t => f(t :: HNil)(ctx))
.onFailure { case ex: Exception =>
ctx.reject(exceptionHandler(ex))
}
}
}
Example usage -
protected def getLogin(account: Account) = futureDirective(
logins.findById(account.id)
)
getAccount(access_token) { account =>
getLogin(account) { login =>
// ...
}
}