Reputation: 810
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...
Upvotes: 1
Views: 397
Reputation: 574
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") {
}
}
}
}
)
Upvotes: 1