I have the following piece of code that makes a request to a HTTP service to fetch a resource. It could be that the service is not available or the resource is not available. So I need to handle such error scenarios. The server actually throws an exception that I have to handle in my Scala client. Here is what I have done so far:
def getData(resource: String, requestParam: Option[Seq[(String, String)]]): Try[Elem] = {
Try {
val wc = WebClient.create(address(scheme, host, port) + "/" + resource, un, pw, null)
// Load the trust store to the HTTPConduit
if ("https" == scheme) {
val conduit = WebClient.getConfig(wc).getConduit.asInstanceOf[HTTPConduit]
loadTrustStore(conduit)
}
requestParam match {
case Some(reqParams) => reqParams.foreach((param: (String, String)) => {
wc.query(param._1, param._2)
})
case None => wc
}
XML.loadString("""<?xml version="1.0" encoding="UTF-8"?>""" + wc.get(classOf[String]))
}
}
Now, the caller of this method looks like this:
def prepareDomainObject(): MyDomainObject = {
getData(resource, params) match {
case Success(elem) => getDomainFromElem(elem)
case Failure(_) => ?? What do I do here? I actually want to get the message in the Throwable and return another type. How do I get around this?
}
How do I handle my match condition such that I return a different type in case of an error?
You have two options. Either have prepareDomainObject return a Try as well:
def prepareDomainObject(): Try[MyDomainObject] = {
getData(resource, params) map getDomainFromElem
}
or have it throw an exception on error:
def prepareDomainObject(): MyDomainObject = {
getDomainFromElem(getData(resource, params).get)
}
Related
I have ServerA which exposes an API method for a client, which looks like this:
def methodExposed()= Action.async(json) { req =>
val reqAsModel = request.body.extractOpt[ClientRequestModel]
reqAsModel match {
case Some(clientRequest) =>
myApiService
.doSomething(clientRequest.someList)
.map(res => ???)
case None =>
Future.successful(BadRequest("could not extract request"))
}
}
So, I have a case class for the client request and if I cannot extract it from the request body, then I return a BadRequest with the message and otherwise I call an internal apiService to perform some action with this request.
doSomething performs an API call to ServerB that can return 3 possible responses:
200 status
400 status with body that I need to extract to a case class
500 status
doSomething looks like this:
def doSomething(list: List[String]) = {
wSClient.url(url).withHeaders(("Content-Type", "application/json")).post(write(list)).map { response =>
response.status match {
case Status.BAD_REQUEST =>
parse(response.body).extract[ServiceBResponse]
case Status.INTERNAL_SERVER_ERROR =>
val ex = new RuntimeException(s"ServiceB Failed with status: ${response.status} body: ${response.body}")
throw ex
}
}
}
Now I have two issues:
Since the 200 returns with no body and 400 has a body, I don't know what should be the return type of doSomething
How should I handle this in the controller and return the response to the client properly in methodExposed?
I would do something like this:
case class ServiceBResponse(status: Int, body: Option[String] = None)
And then, doSomething would be like:
def doSomething(list: List[String]) = {
wSClient.url(url).withHeaders(("Content-Type", "application/json")).post(write(list)).map { response =>
response.status match {
case Status.OK =>
ServiceBResponse(response.status)
case Status.BAD_REQUEST =>
ServiceBResponse(response.status, Option(response.body))
case Status.INTERNAL_SERVER_ERROR =>
val message = s"ServiceB Failed with status: ${response.status} body: ${response.body}"
ServiceBResponse(response.status, Option(message))
}
}
}
Finally, inside the controller:
def methodExposed() = Action.async(json) { req =>
val reqAsModel = request.body.extractOpt[ClientRequestModel]
reqAsModel match {
case Some(clientRequest) =>
myApiService
.doSomething(clientRequest.someList)
.map(serviceBResponse => Status(serviceBResponse.status)(serviceBResponse.getOrElse("")))
case None =>
Future.successful(BadRequest("could not extract request"))
}
}
Another alternative is directly use WSResponse:
def doSomething(list: List[String]) = {
wSClient
.url(url)
.withHeaders(("Content-Type", "application/json"))
.post(write(list))
}
And the controller:
def methodExposed() = Action.async(json) { req =>
val reqAsModel = request.body.extractOpt[ClientRequestModel]
reqAsModel match {
case Some(clientRequest) =>
myApiService
.doSomething(clientRequest.someList)
.map(wsResponse => Status(wsResponse.status)(wsResponse.body))
case None =>
Future.successful(BadRequest("could not extract request"))
}
}
If 400 is a common expected error, I think the type Future[Either[Your400CaseClass, Unit]] makes sense. In terms of how methodExposed returns the result to the client depends on your business logic:
Is the underlying 400 something the client should be informed of? If methodExposed should return a 500 to the client when doSomething encounters a 400
Otherwise you can propagate the error to the client. Depending on the business logic, you may or may not want to transform your case class into another form, and potentially with a different http code.
You should throw an exception (using Future.failed) if doSomething returns 500 (or more generally, any unexpected http code).
(Lastly, I hope you're not using 500 to communicate a 'normal' error like validation / authentication error. 5xx code should only be used for exceptional and unrecoverable errors. Most http clients I know will throw an exception immediately when a 5xx is encountered, which means the user won't get the chance to handle it)
In Scala Play and Slick, I wish to send a OK response only after a record has been created in the database, so far I've got:
def createItem = Action(BodyParsers.parse.json) {
request => {
val result = request.body.validate[Item]
result.fold(
invalid => {
val problem = new Problem(BAD_REQUEST, "Invalid Item JSON", invalid.toString)
returnProblemResult(BadRequest, problem)
},
item => {
service.create(item)
// TODO check for success before sending ok
Ok.as(ContentTypes("DEFAULT"))
}
)
}
}
And inside the service.create method:
def create(item: Item): Future[Unit] = {
exists(item.id).map {
case true =>
case _ => db.run(Item.table += cc)
}
}
Currently, the OK response get sent even if no new item is created. I would like it to only return OK if an item is created. If the item already exists, or if there're other errors (e.g. database errors), the createItem method should know what kind of problem occurred and return a Problem result with error message.
Try this:-
For service, change the method to
def create(item: Item): Future[Int] = {
exists(item.id).map {
case true => 0
case _ => db.run(Item.table += cc)
}
}
In controller, do
service.create(item).map {
case i:Int=> Ok.as(ContentTypes("DEFAULT"))
}.recoverWith {
// To handle the error in the processing
case ex: Throwable => InternalServerError(ex)
}
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)
}
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 =>
// ...
}
}
Appears I am missing something but here is what I have got (posting only relevant piece). where MyService.save returns Future[Option[MyCompany] ].
def myPost = Action.async(parse.json) { request =>
val mn = Json.fromJson[MyEntity](request.body)
mn.map{
case m : MyEntity => MyService.save(m).map{f=>
f.map(mm=> Ok(mm ) )
}
}.recoverTotal {
e => Future { BadRequest("Detected error:" + JsError.toFlatJson(e)) }
}
}
Although I have defined
implicit val companyWriter: Writes[MyCompany] = (...)
And this implicit is in the scope, it shows compile error
Cannot write an instance of MyCompany to HTTP response. Try to define
a Writeable[MyCompany]
FYI: This writer is used elsewhere where I do Json.toJson(myCompany) and over there it finds and works fine.
Anything in particular to async Ok that it's missing?
EDIT
It appears that Ok() method cannot figure out the MyCompany needs to be transformed to json. following seems to have worked.
Ok(Json.toJson(mm) )
Is this because arguments to Ok can vary? Separately there are too many "map" in the above code. Any recommendation for improvement and making it more concise ?
Your compiler error is about a Writeable, not a Writes. Writeables are used to convert whatever you have to something that can be written to an HTTP response, Writes are used to marshall objects to JSON. The names can be a little confusing.
As for style...
def myPost = Action.async(parse.json) { request =>
request.body.validate[MyEntity] map { myEntity =>
MyService.save(myEntity).map { maybeCompany =>
maybeCompany match {
case Some(company) => Ok(Json.toJson(company))
case None => NoContent // or whatever's appropriate
}
}
} recoverTotal { t =>
Future { BadRequest("Detected error: " + JsError.toFlatJson(e)) }
}
}