Scala: How to get the result of a Future
Solution 1
The function you wrote will not work as you would think. It would (likely) first return true
and later set the result
variable.
Normally you would do something like this:
isTokenExpired(token).map { result =>
// do stuff
}
In a framework like Play you would map the Future
to an http response and give Play back a Future[SimpleResult]
. Play knows how to handle Future
results.
In general it's recommended you do not wait for a Future
to complete in production code, but work with the values in the Future
and let the framework you are using handle the result.
In tests it might come in handy to wait for a result, you can do that like this:
Await.result(someFuture, 5.seconds)
Edit
I would probably extract the construction of a token so that I end up with a Future[Token]
. That allows me to more easily compose things. It also allows me to create code that has a better architecture and is easier to test.
I would probably break down the code into more smaller methods, but the example below gives you an idea of the direction I would take.
class TokenService(connection: MongoConnection) {
def tokenFor(tokenString: String): Future[Token] = ???
}
class SecuredAction(tokenService: TokenService) extends
ActionBuilder[ApiRequest] {
import play.api.libs.concurrent.Execution.Implicits._
def invokeBlock[A](request: Request[A], block: (ApiRequest[A]) => Future[SimpleResult]) =
extractTokenFrom(request) match {
case Some(tokenString) => {
tokenService.tokenFor(tokenString) flatMap {
case token if (!token.isValid) =>
Logger.warn(s"request ${request.uri} not authorized: token ${token.id} has been tampered")
Future.successful(Unauthorized(AuthErrors.authenticationViolated(token.subject)(request).asJson))
case token if (token.isExpired) =>
Logger.debug(s"request ${request.uri} not authorized: token ${token.id} has expired")
Future.successful(Unauthorized(AuthErrors.authenticationExpired(token.subject)(request).asJson))
case token if (!token.isAuthorized) =>
Logger.info(s"request ${request.uri} not authorized: required claims not defined for account ${token.subject}")
Future.successful(Forbidden(AuthErrors.requestNotAuthorized(token.subject)(request).asJson))
case token =>
Logger.debug(s"request ${request.uri} authorized for account ${token.subject}")
block(new ApiRequest(token, request))
}
}
case _ =>
Logger.debug(s"request ${request.uri} not authenticated")
Future.successful(Unauthorized(
AuthErrors.requestNotAuthenticated()(request).asJson).withHeaders(HeaderNames.WWW_AUTHENTICATE -> AuthType))
}
val AuthType = "MyAuthType"
val TokenHeader = s"""$AuthType (.*)""".r
def extractTokenFrom(request: RequestHeader) = {
val authorizationHeader = request.headers.get(HeaderNames.AUTHORIZATION)
authorizationHeader flatMap {
case TokenHeader(token) => Some(token.trim)
case _ => None
}
}
}
Solution 2
One solution would be to make future current by literally waiting for the result forever:
Await.result(futureResult, Duration.Inf)
Waiting forever transforms a Future[Value]
into a Value
which is what you want.
That's how I love to comment this move in my code...
// wait forever to make future current (?)
Obs. I infinite duration only in my ScalaTest. For production, the waiting period should be tuned as done by @EECOLOR.
Related videos on Youtube
j3d
Updated on June 30, 2022Comments
-
j3d almost 2 years
I've a method that returns a
Future
like this...def isTokenExpired(token: String): Future[Boolean] = { ... }
... and then I've another method that invokes
isTokenExpired
that returns aBoolean
like this:def isExpired(token: String): Boolean = { var result = true isTokenExpired(token).onComplete { case Success(r) => result = r case Failure(_) => result = true } result }
Is there a better way to write the
isExpired
method?EDIT
As requested by EECOLOR, let me provide you with more details. For my Play application I've implemented an authorization mechanism based on JSON Web Token (jwt). All the claims are contained in the jwt except the expiration time, which is stored in a MongoDB collection. Here below is a summary of how my
Token
class looks like:class Token { ... def id: String = { ... } def issueTime: LocalDateTime = { ... } def issuer: String = { ... } ... def isValid: Boolean = { ... } def isExpired: Boolean = { /* uses ReactiveMongo to access MongoDB */ } }
As you can see, all the jwt properties are self-contained except expiration info. Method
isExpired
uses ReactiveMongo, which always returns aFuture
. To make things even more complex, I use this jwt in a customizedAction
like this:class SecuredAction[T <: Controller] private(private val methodName: String) extends ActionBuilder[ApiRequest] { ... def invokeBlock[A](request: Request[A], block: (ApiRequest[A]) => Future[SimpleResult]) = {{ request.headers.get(HeaderNames.AUTHORIZATION) match { case Some(header) => s"""$AuthType (.*)""".r.unapplySeq(header).map(_.head.trim) case _ => None }} match { case Some(tokenString) => { val token = Token(tokenString) if (!token.isValid) { Logger.warn(s"request ${request.uri} not authorized: token ${token.id} has been tampered") Future.successful(Unauthorized(AuthErrors.authenticationViolated(token.subject)(request).asJson)) } else if (token.isExpired) { Logger.debug(s"request ${request.uri} not authorized: token ${token.id} has expired") Future.successful(Unauthorized(AuthErrors.authenticationExpired(token.subject)(request).asJson)) } else if (!isAuthorized(token)) { Logger.info(s"request ${request.uri} not authorized: required claims not defined for account ${token.subject}") Future.successful(Forbidden(AuthErrors.requestNotAuthorized(token.subject)(request).asJson)) } else { Logger.debug(s"request ${request.uri} authorized for account ${token.subject}") block(new ApiRequest(token, request)) } } case _ => { Logger.debug(s"request ${request.uri} not authenticated") Future.successful(Unauthorized( AuthErrors.requestNotAuthenticated()(request).asJson ).withHeaders(HeaderNames.WWW_AUTHENTICATE -> AuthType)) } } }
As you can see, I need to return a
Future[play.mvc.results.Result]
, not aFuture[Boolean]
as would returnisExpired
if I usedFuture.map
. Do you get to point? -
j3d about 10 yearsYes... I know... but in my case I need a Boolean, not a Future[Boolean]. Is Await.result so bad in production code?
-
om-nom-nom about 10 yearsand I love to comment such code as
// this is how deadlocks are born
-
user3504561 about 10 yearsYou can tune it as @EECOLOR did but then it gets less suspense.
-
user3504561 about 10 years@om-nom-nom I have edited my answer. Thank you for your note which brought me back to reality.
-
Ryan about 10 yearsUsing
Await.result
or its cousinAwait.ready
in production code is likely to lead to deadlocks. -
j3d about 10 yearsI've just added more info to my post so that you can understand why I need a Boolean instead of a Future[Boolean]. Thanks.
-
j3d about 10 yearsOK, I see and just confirm my vote to your answer ;-) Thanks a lot.
-
user3504561 about 10 years@Ryan. What's wrong in using
Await.result
in a ScalaTestFunSuite
? @EECOLOR sees it as handy. -
EECOLOR over 8 years@user3504561 You should never use
Await.result
in production code. Especially in web frameworks, this quickly fills up the thread pool. On top of that, it opens up the possibility for the elusive deadlock problem. This is a state where one side is waiting for the other side to finish waiting. As I stated in my answer, it's fine to use in a test suite, because generally you are using it only to extract and test the result. -
Rakshith almost 8 yearsIam not sure if Await.result is the right way to do it