diff options
Diffstat (limited to 'src/main/scala/xyz/driver/core/rest')
4 files changed, 153 insertions, 7 deletions
diff --git a/src/main/scala/xyz/driver/core/rest/DriverRoute.scala b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala new file mode 100644 index 0000000..eb9a31a --- /dev/null +++ b/src/main/scala/xyz/driver/core/rest/DriverRoute.scala @@ -0,0 +1,109 @@ +package xyz.driver.core.rest + +import java.sql.SQLException + +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RequestContext, Route} +import com.typesafe.scalalogging.Logger +import org.slf4j.MDC +import xyz.driver.core.rest +import xyz.driver.core.rest.errors._ + +import scala.compat.Platform.ConcurrentModificationException + +trait DriverRoute { + def log: Logger + + def route: Route + + def routeWithDefaults: Route = { + (defaultResponseHeaders & handleExceptions(ExceptionHandler(exceptionHandler)))(route) + } + + protected def defaultResponseHeaders: Directive0 = { + (extractRequest & optionalHeaderValueByType[Origin](())) tflatMap { + case (request, originHeader) => + val tracingHeader = RawHeader(ContextHeaders.TrackingIdHeader, rest.extractTrackingId(request)) + val responseHeaders = List[HttpHeader]( + tracingHeader, + allowOrigin(originHeader), + `Access-Control-Allow-Headers`(AllowedHeaders: _*), + `Access-Control-Expose-Headers`(AllowedHeaders: _*) + ) + + respondWithHeaders(responseHeaders) + } + } + + /** + * Override me for custom exception handling + * + * @return Exception handling route for exception type + */ + protected def exceptionHandler: PartialFunction[Throwable, Route] = { + case serviceException: ServiceException => + serviceExceptionHandler(serviceException) + + case is: IllegalStateException => + ctx => + log.warn(s"Request is not allowed to ${ctx.request.method} ${ctx.request.uri}", is) + errorResponse(ctx, StatusCodes.BadRequest, message = is.getMessage, is)(ctx) + + case cm: ConcurrentModificationException => + ctx => + log.warn(s"Concurrent modification of the resource ${ctx.request.method} ${ctx.request.uri}", cm) + errorResponse(ctx, + StatusCodes.Conflict, + "Resource was changed concurrently, try requesting a newer version", + cm)(ctx) + + case se: SQLException => + ctx => + log.warn(s"Database exception for the resource ${ctx.request.method} ${ctx.request.uri}", se) + errorResponse(ctx, StatusCodes.InternalServerError, "Data access error", se)(ctx) + + case t: Exception => + ctx => + log.warn(s"Request to ${ctx.request.method} ${ctx.request.uri} could not be handled normally", t) + errorResponse(ctx, StatusCodes.InternalServerError, t.getMessage, t)(ctx) + } + + protected def serviceExceptionHandler(serviceException: ServiceException): Route = { + val statusCode = serviceException match { + case e: InvalidInputException => + log.info("Invalid client input error", e) + StatusCodes.BadRequest + case e: InvalidActionException => + log.info("Invalid client action error", e) + StatusCodes.Forbidden + case e: ResourceNotFoundException => + log.info("Resource not found error", e) + StatusCodes.NotFound + case e: ExternalServiceException => + log.error("Error while calling another service", e) + StatusCodes.InternalServerError + case e: ExternalServiceTimeoutException => + log.error("Service timeout error", e) + StatusCodes.GatewayTimeout + case e: DatabaseException => + log.error("Database error", e) + StatusCodes.InternalServerError + } + + { (ctx: RequestContext) => + errorResponse(ctx, statusCode, serviceException.message, serviceException)(ctx) + } + } + + protected def errorResponse[T <: Exception](ctx: RequestContext, + statusCode: StatusCode, + message: String, + exception: T): Route = { + val trackingId = rest.extractTrackingId(ctx.request) + MDC.put("trackingId", trackingId) + complete(HttpResponse(statusCode, entity = message)) + } +} diff --git a/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala b/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala index 1e95811..376b154 100644 --- a/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala +++ b/src/main/scala/xyz/driver/core/rest/HttpRestServiceTransport.scala @@ -4,9 +4,12 @@ import akka.actor.ActorSystem import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.Materializer +import akka.stream.scaladsl.TcpIdleTimeoutException import com.typesafe.scalalogging.Logger import org.slf4j.MDC import xyz.driver.core.Name +import xyz.driver.core.rest.errors.{ExternalServiceException, ExternalServiceTimeoutException} import xyz.driver.core.time.provider.TimeProvider import scala.concurrent.{ExecutionContext, Future} @@ -55,18 +58,27 @@ class HttpRestServiceTransport(applicationName: Name[App], log.warn(s"Failed to receive response from ${request.method} ${request.uri} in $responseLatency ms", t) }(executionContext) - response + response.recoverWith { + case _: TcpIdleTimeoutException => + val serviceCalled = s"${requestStub.method} ${requestStub.uri}" + Future.failed(ExternalServiceTimeoutException(serviceCalled)) + case t: Throwable => Future.failed(t) + } } - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { + def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest)( + implicit mat: Materializer): Future[Unmarshal[ResponseEntity]] = { - sendRequestGetResponse(context)(requestStub) map { response => + sendRequestGetResponse(context)(requestStub) flatMap { response => if (response.status == StatusCodes.NotFound) { - Unmarshal(HttpEntity.Empty: ResponseEntity) + Future.successful(Unmarshal(HttpEntity.Empty: ResponseEntity)) } else if (response.status.isFailure()) { - throw new Exception(s"Http status is failure ${response.status} for ${requestStub.method} ${requestStub.uri}") + val serviceCalled = s"${requestStub.method} ${requestStub.uri}" + Unmarshal(response.entity).to[String] flatMap { error => + Future.failed(ExternalServiceException(serviceCalled, error)) + } } else { - Unmarshal(response.entity) + Future.successful(Unmarshal(response.entity)) } } } diff --git a/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala new file mode 100644 index 0000000..e91a3c2 --- /dev/null +++ b/src/main/scala/xyz/driver/core/rest/errors/serviceException.scala @@ -0,0 +1,23 @@ +package xyz.driver.core.rest.errors + +sealed abstract class ServiceException extends Exception { + def message: String +} + +final case class InvalidInputException(override val message: String = "Invalid input") extends ServiceException + +final case class InvalidActionException(override val message: String = "This action is not allowed") + extends ServiceException + +final case class ResourceNotFoundException(override val message: String = "Resource not found") + extends ServiceException + +final case class ExternalServiceException(serviceName: String, serviceMessage: String) extends ServiceException { + override def message = s"Error while calling '$serviceName': $serviceMessage" +} + +final case class ExternalServiceTimeoutException(serviceName: String) extends ServiceException { + override def message = s"$serviceName took too long to respond" +} + +final case class DatabaseException(override val message: String = "Database access error") extends ServiceException diff --git a/src/main/scala/xyz/driver/core/rest/package.scala b/src/main/scala/xyz/driver/core/rest/package.scala index 942ca3a..531cd8a 100644 --- a/src/main/scala/xyz/driver/core/rest/package.scala +++ b/src/main/scala/xyz/driver/core/rest/package.scala @@ -6,6 +6,7 @@ import akka.http.scaladsl.model.{HttpRequest, HttpResponse, ResponseEntity, Stat import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import akka.http.scaladsl.unmarshalling.Unmarshal +import akka.stream.Materializer import akka.stream.scaladsl.Flow import akka.util.ByteString import xyz.driver.tracing.TracingDirectives @@ -25,7 +26,8 @@ trait ServiceTransport { def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] + def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest)( + implicit mat: Materializer): Future[Unmarshal[ResponseEntity]] } final case class Pagination(pageSize: Int, pageNumber: Int) |