This question already has an answer here:
Akka-HTTP route not found?
(1 answer)
Closed 1 year ago.
I have the following route setup
val routes = protectedRoute("wallet") { user =>
concat {
path("wallet" / "deposit") {
get {
completeEitherT(walletService.deposit(user._id, "dollars", 50))
}
}
path("wallet") {
get {
completeEitherT(walletService.getForUser(user._id))
}
}
}
}
The completeEitherT function is as follows
def completeEitherT[T](t: EitherT[Future, DatabaseError, T])(implicit marsh: ToResponseMarshaller[T]): Route = {
onSuccess(t.value) {
case Left(x: DatabaseErrors.NotFound) =>
logger.error(x.toString)
complete(StatusCodes.NotFound)
case Left(error) => complete(StatusCodes.InternalServerError)
case Right(value) => complete(value)
}
}
The protectedRoute Directive is:
def protectedRoute(permissions: String*): Directive1[User] = new Directive1[User] {
override def tapply(f: (Tuple1[User]) => Route): Route = {
headerValueByName("Authorization") { jwt =>
Jwt.decode(jwt, jwtSecret, List(algo)) match {
case Failure(_) =>
logger.error("Unauthorized access from jwtToken:", jwt)
complete(StatusCodes.Unauthorized)
case Success(value) =>
val user = JsonParser(value.content).convertTo[User]
if (user.roles.flatMap(_.permissions).exists(permissions.contains)) f(Tuple1(user))
else complete(StatusCodes.Forbidden)
}
}
}
}
The issue that I am having is that whichever path is defined first results in a 404 when called via Postman.
The requested resource could not be found.
This is not a response from completeEitherT as it does not log the error
How do I define two or more paths inside the same directive?
Please mark as duplicate, I could not find this on Google Search but SO showed this as related.
Answer
Essentially I am missing a ~ between the paths
val routes = protectedRoute("wallet") { user =>
concat {
path("wallet" / "deposit") {
get {
completeEitherT(walletService.deposit(user._id, "dollars", 50))
}
} ~
path("wallet") {
get {
completeEitherT(walletService.getForUser(user._id))
}
}
}
}
Related
I have the following routes definition
private val routes: Route =
concat(
pathEnd {
get {
handleErrorsAndReport("list_foos") {
}
}
},
path(Segment) { fooId =>
get {
handleErrorsAndReport("get_foo") {
rejectEmptyResponse(
complete(service.do(fooId))
)
}
}
},
pathEnd {
authenticateBasic(
realm = "Secure scope",
scopesAuthenticator
) { scopes =>
post {
handleErrorsAndReport("create_foo") {
}
}
}
},
path(Segment) { fooId =>
authenticateBasic(
realm = "Secure scopes",
scopesAuthenticator
) { scopes =>
concat(
put {
handleErrorsAndReport("update_foo") {
}
},
delete {
handleErrorsAndReport("delete_foo") {
}
}
)
}
}
)
I am trying to consume the get_foo endpoint. I have created a unit test for that and it looks like this
"allow get operation on foo without authentication present" in {
Get("/foos/{some_id}") ~> routes ~> check {
status shouldBe StatusCodes.NotFound
}
}
While debugging the test I can see that the route is correctly identified and I can access the code inside the route. The service code inside the get_foo route produces a None and complete(None) creates a rejection since it's an empty response and I have the rejectEmptyResponse directive. So I would expect that I would get a 404 response based on the handleErrorsAndReport directive that I have defined. The error handler looks like this
private def handleErrorsAndReport(endpoint: String): Directive0 = extractRequestContext.flatMap { ctx =>
val start = System.currentTimeMillis()
mapResponse { resp =>
// store response related metrics and return response
resp
} & handleExceptions(exceptionHandler)
}
private val exceptionHandler: ExceptionHandler = {
def handle(e: Throwable, responseCode: StatusCode, errorMessage: Option[String]): Route = {
extractRequest { request =>
val response = (responseCode, errorMessage) match {
case (InternalServerError, _) => "Internal Server Error"
case (_, Some(message)) => message
case _ => "Bad Request"
}
complete(HttpResponse(responseCode, entity = response))
}
}
ExceptionHandler {
case e#AError(description) => handle(e, BadRequest, Some(description))
case e: BError => handle(e, InternalServerError, Some(e.errorMessage))
case e: CError => handle(e, BadRequest, Some(e.errorMessage))
case e: DError => handle(e, BadRequest, Some(e.errorMessage))
case e: EError => handle(e, BadRequest, Some(e.errorMessage))
case e#FException(filter) => handle(e, BadRequest, Some(s"bla"))
case other => handle(other, InternalServerError, Option(other.getMessage))
}
}
What I am getting though is a 401 Unauthorized. How can this be?
As I was debugging the code I noticed that the control flow never enters my exception handler - I added breakpoints everywhere inside...
The problem with your code is that, after the rejection of request in the unauthenticated directive, the match happens against an authenticated route with the same url. So the authentication fails and results in 401 unauthorized instead of 404.
For solving it, you need to prevent this match against the authenticated route, after the failure of the unauthenticated route with the GET method, by wrapping it inside post, put, and delete routes so that the GET requests cannot reach it.
So it can be written as
private val routes: Route =
concat(
pathEnd {
get {
handleErrorsAndReport("list_foos") {
}
}
},
path(Segment) { fooId =>
get {
handleErrorsAndReport("get_foo") {
rejectEmptyResponse(
complete(service.do(fooId))
)
}
}
},
post {
pathEnd {
authenticateBasic(
realm = "Secure scope",
scopesAuthenticator
) { scopes =>
handleErrorsAndReport("create_foo") {
}
}
}
},
put{
path(Segment) { fooId =>
authenticateBasic(
realm = "Secure scopes",
scopesAuthenticator
) { scopes =>
handleErrorsAndReport("update_foo") {
}
}
}
},
delete{
path(Segment) { fooId =>
authenticateBasic(
realm = "Secure scopes",
scopesAuthenticator
) { scopes =>
handleErrorsAndReport(“delete_foo") {
}
}
}
}
)
I want to have a base route that receive a IntNumber and make some checks with the database to know if the values is correct or not and then If the value is correct I want to propagate the value to child routes.
Base Route
class BaseServiceRoute extends PathDirectives with SecurityDirectives {
val baseUserRoute = pathPrefix("user" / IntNumber)
}
How can I make some checks for the value IntNumber and the delegate the value to the child route? Directives?
Child Route
class CategoryServiceRoute(implicit executionContext: ExecutionContext) extends BaseServiceRoute {
val route = baseUserRoute { userId =>
pathPrefix("category") {
pathEndOrSingleSlash {
get {
complete(s"$userId")
}
} ~
path(LongNumber) { categoryId =>
get {
complete(s"$categoryId")
} ~
post {
complete("Hello category post")
}
}
}
}
}
Thanks
The best-practice suggestion would be just to nest the routes so that you can still access the value from the outer route like this:
pathPrefix("user" / IntNumber) { userId =>
pathPrefix("category") {
pathEndOrSingleSlash {
get {
complete(s"$userId")
}
}
}
}
However, you seem to want to separate routes into multiple parts which is perfectly fine. In that case just make the child route a def:
def childRoute(userId: Int): Route =
pathPrefix("category") {
pathEndOrSingleSlash {
get {
complete(s"$userId")
}
}
}
and then use it like this:
baseUserRoute(childRoute)
I'm novice to scala and Akka-Http. Experimenting Akka-Http for writing rest services. I have to return json or protobuf based on the Accept header.
optionalHeaderValueByName("Accept"){ contentType =>
if(contentType == Some(protoEncode)) {
complete {
NewsService.getNewsList().map {
case stories: List[Story] => HttpResponse(entity = HttpEntity(ContentType(protoEncoding), StoryList(stories).toProto().build().toByteArray))
}
}
} else {
complete {
NewsService.getNewsList().map {
case stories: List[Story] => StoryList(stories)
}
}
}
As you can see the code repetition is happening can anyone suggest what could be the best way to optimise and generalise design to avoid such situation.
The simplest way is to move the check inside the body.
optionalHeaderValueByName("Accept"){ contentType =>
complete {
NewsService.getNewsList().map {
case stories: List[Story] =>
if(contentType == Some(protoEncode)) {
HttpResponse(entity = HttpEntity(ContentType(protoEncoding), StoryList(stories).toProto().build().toByteArray))
} else
StoryList(stories)
}
}
}
Figured it out.
optionalHeaderValueByName("Accept") { contentType =>
onSuccess(NewsService.getNewsList()) {
case stories: List[Story] => contentType match {
case Some(protoEncodingString) => complete(HttpResponse(entity = HttpEntity(ContentType(protoEncoding), StoryList(stories).toProto().build().toByteArray)))
case _=> complete(StoryList(stories))
}
}
}
I have an article collection where fields "title" and "publication" has a unique combined key constraint.
When calling insertOrUpdateArticle(a: Article) it will first try to insert it, in case it hits the constraint, it should update the article - if needed.
However, I'm stuck before that. Current error is:
Error:(88, 57) type mismatch;
found : scala.concurrent.Future[scala.concurrent.Future[Boolean]]
required: Boolean
col_article.find(selector).one[Article].map {
Source:
def insertOrUpdateArticle(a: Article): Future[Boolean] = {
// try insert article
col_article.insert[Article](a).map {
// article inserted
lastError => {
println("Article added.")
true
}
}.recover {
case lastError: LastError =>
// check if article existed
lastError.code.get match {
case 11000 => {
// article existed (duplicate key error)
// load old article
val selector = BSONDocument(
"title" -> BSONString(a.title),
"publication" -> BSONString(a.publication)
)
col_article.find(selector).one[Article].map {
case Some(old_a: Article) => {
// TODO: compare a with old_a
// TODO: if a differs from old_a, update
Future(true)
}
case None => {
// something went wrong
Future(false)
}
}
}
case _ => {
println("DB.insertOrUpdateArticle() unexpected error code when inserting: " + lastError.code.get)
false
}
}
case ex =>
println("DB.insertOrUpdateArticle() unexpected exception when inserting: " + ex)
false
}
}
I'm unsure what to do here. The code should return Future(true) if the article was saved or updated, false otherwise. There's something with reactivemongo and/or scala futures I'm missing out here.
Using recoverWith to create new Future is the solution, modified code:
def insertOrUpdateArticleOld(a: Article): Future[Boolean] = {
// try insert article
col_article.insert[Article](a).map {
// article inserted
lastError => {
println("Article added.")
true
}
}.recoverWith {
case lastError: LastError =>
// check if article existed
lastError.code.get match {
case 11000 => {
// article existed (duplicate key error)
// load old article
val selector = BSONDocument(
"title" -> BSONString(a.title),
"publication" -> BSONString(a.publication)
)
col_article.find(selector).one[Article].flatMap {
case Some(old_a: Article) => {
// TODO: compare a with old_a
// TODO: if a differs from old_a, update
Future(true)
}
case None => {
// something went wrong
Future(false)
}
}
}
case _ => {
println("DB.insertOrUpdateArticle() unexpected error code when inserting: " + lastError.code.get)
Future(false)
}
}
case ex =>
println("DB.insertOrUpdateArticle() unexpected exception when inserting: " + ex)
Future(false)
}
}
I would like to write a custom directive for all get, post,put and delete requests, because all requests needs authorization. To keep code DRY, I want to abstract those boilerplate(I have to authorize more than 100 requests). I could handle all Get Requests as follows
def method1 = Try("hi") // controller methods
def method2 = Try("hello")
path("getData1") {
handleGetRequest(method1)
}
path("getData2") {
handleGetRequest(method2)
}
def customGetDirective: Directive0 = {
headerValueByName("token").flatMap { token =>
authorize(checkAuthorization(token, ""))
get.hflatMap {x=>
respondWithMediaType(`application/json`)
}
}
}
def handleGetRequest(fn: => Try[_]) = {
customGetDirective { ctx=>
val result = fn
val json = //result can be coverted to json
ctx.complete(json)
}
}
I want to write a similar Generic directive for POST request, so that I could use
path("postData1") {
handlePostRequest(saveToDb)
}
def handlePostRequest(fn: => Try[_]) = {
customPostDirective { (ctx, JsonData)=>
//.......
ctx.complete(json)
}
}
but, I dont know how to get RequestContext and JsonData as tuple
I can write like this,
entity(as[String]) { jsonData =>
customPostDirective{ ctx=>
}
}
If I get a tuple it will be more useful.
Thank you.