Skip to content

Commit afa96d0

Browse files
committed
update Api Errors adding support for WWW-Authenticate header, update rejection and exception default handlers
1 parent 966b10b commit afa96d0

File tree

2 files changed

+36
-26
lines changed

2 files changed

+36
-26
lines changed

server/src/main/scala/app/softnetwork/api/server/ApiErrors.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import akka.http.scaladsl.server.Directives._
55
import akka.http.scaladsl.server.Route
66
import app.softnetwork.serialization.commonFormats
77
import org.json4s.Formats
8+
import sttp.model.headers.WWWAuthenticateChallenge
89
import sttp.model.{HeaderNames, StatusCode, Uri}
910
import sttp.tapir.EndpointOutput.OneOf
1011
import sttp.tapir.server.PartialServerEndpointWithSecurityOutput
@@ -70,6 +71,23 @@ object ApiErrors extends SchemaDerivation with TapirJson4s {
7071
}
7172
)(_.left.get.toString())
7273

74+
case class UnauthorizedWithChallenge(scheme: String, realm: String) extends ErrorInfo {
75+
override val message: String = "Unauthorized"
76+
override def toString: String = WWWAuthenticateChallenge(scheme).realm(realm).toString()
77+
}
78+
79+
implicit val unauthorizedWithChallengeCodec
80+
: Codec[String, UnauthorizedWithChallenge, CodecFormat.TextPlain] =
81+
Codec.string.mapDecode(s =>
82+
WWWAuthenticateChallenge.parseSingle(s) match {
83+
case Right(challenge) =>
84+
DecodeResult.Value(
85+
UnauthorizedWithChallenge(challenge.scheme, challenge.realm.getOrElse(""))
86+
)
87+
case Left(_) => DecodeResult.Error(s, new Exception("Cannot parse WWW-Authenticate header"))
88+
}
89+
)(_.toString())
90+
7391
implicit def apiError2Route(apiError: ErrorInfo)(implicit formats: Formats): Route =
7492
apiError match {
7593
case r: BadRequest => complete(HttpResponse(StatusCodes.BadRequest, entity = r))
@@ -134,13 +152,20 @@ object ApiErrors extends SchemaDerivation with TapirJson4s {
134152
.example(ApiErrors.ErrorMessage("Test error message"))
135153
)
136154

155+
val unauthorizedWithChallengeVariant: EndpointOutput.OneOfVariant[UnauthorizedWithChallenge] =
156+
oneOfVariant(
157+
statusCode(StatusCode.Unauthorized)
158+
.and(header[UnauthorizedWithChallenge](HeaderNames.WwwAuthenticate))
159+
)
160+
137161
val oneOfApiErrors: EndpointOutput.OneOf[ApiErrors.ErrorInfo, ApiErrors.ErrorInfo] =
138162
oneOf[ApiErrors.ErrorInfo](
139163
// returns required http code for different types of ErrorInfo.
140164
// For secured endpoint you need to define
141165
// all cases before defining security logic
142166
forbiddenVariant,
143167
unauthorizedVariant,
168+
unauthorizedWithChallengeVariant,
144169
notFoundVariant,
145170
foundVariant,
146171
badRequestVariant,
@@ -226,6 +251,7 @@ object ApiErrors extends SchemaDerivation with TapirJson4s {
226251
body.errorOutVariants(
227252
forbiddenVariant,
228253
unauthorizedVariant,
254+
unauthorizedWithChallengeVariant,
229255
notFoundVariant,
230256
foundVariant,
231257
badRequestVariant,

server/src/main/scala/app/softnetwork/api/server/ApiRoutes.scala

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import akka.http.scaladsl.server.{
1414
Route,
1515
ValidationRejection
1616
}
17+
import akka.http.scaladsl.settings.RoutingSettings
1718
import app.softnetwork.api.server.config.ServerSettings
1819
import org.json4s.Formats
1920
import app.softnetwork.serialization._
@@ -31,33 +32,9 @@ trait ApiRoutes extends Directives with GrpcServices with DefaultComplete {
3132

3233
def log: Logger
3334

34-
val rejectionHandler: RejectionHandler =
35-
RejectionHandler
36-
.newBuilder()
37-
.handle { case MissingCookieRejection(cookieName) =>
38-
complete(HttpResponse(StatusCodes.BadRequest, entity = s"$cookieName cookie required"))
39-
}
40-
.handle { case AuthorizationFailedRejection =>
41-
complete(StatusCodes.Forbidden)
42-
}
43-
.handle { case ValidationRejection(msg, _) =>
44-
complete(HttpResponse(StatusCodes.InternalServerError, entity = msg))
45-
}
46-
.handleAll[MethodRejection] { methodRejections =>
47-
val names = methodRejections.map(_.supported.name)
48-
complete(
49-
HttpResponse(
50-
StatusCodes.MethodNotAllowed,
51-
entity = s"Supported methods: ${names mkString " or "}!"
52-
)
53-
)
54-
}
55-
.handleNotFound {
56-
complete(HttpResponse(StatusCodes.NotFound, entity = "Not found"))
57-
}
58-
.result()
35+
val rejectionHandler: RejectionHandler = RejectionHandler.default
5936

60-
val exceptionHandler: ExceptionHandler =
37+
lazy val exceptionHandler: ExceptionHandler =
6138
ExceptionHandler { case e: TimeoutException =>
6239
extractUri { uri =>
6340
log.error(
@@ -67,6 +44,13 @@ trait ApiRoutes extends Directives with GrpcServices with DefaultComplete {
6744
complete(HttpResponse(StatusCodes.InternalServerError, entity = "Timeout"))
6845
}
6946
}
47+
.withFallback(
48+
ExceptionHandler.default(
49+
RoutingSettings(
50+
ServerSettings.config
51+
)
52+
)
53+
)
7054

7155
final def mainRoutes: ActorSystem[_] => Route = system => {
7256
val routes = concat((HealthCheckService :: apiRoutes(system)).map(_.route): _*)

0 commit comments

Comments
 (0)