From 9a8b7368224692c1893807ba95c8026ed92ec6d2 Mon Sep 17 00:00:00 2001 From: Kelly Innes Date: Mon, 18 Sep 2017 14:21:56 -0400 Subject: [PATCH] Improve client-side error response messages - add custom exception handler + exception case classes - propagate error messages back to the client with 400s if they're known errors & 500 if they're unknown --- api/src/main/scala/ErrorHandler.scala | 42 ++++++++++++++++++++++++++ api/src/main/scala/Geoprocessing.scala | 12 ++++---- api/src/main/scala/Utils.scala | 3 +- api/src/main/scala/WebServer.scala | 41 ++++++++++++++----------- 4 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 api/src/main/scala/ErrorHandler.scala diff --git a/api/src/main/scala/ErrorHandler.scala b/api/src/main/scala/ErrorHandler.scala new file mode 100644 index 0000000..bb42a04 --- /dev/null +++ b/api/src/main/scala/ErrorHandler.scala @@ -0,0 +1,42 @@ +package org.wikiwatershed.mmw.geoprocessing + +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server._ +import StatusCodes.{BadRequest,InternalServerError} +import Directives._ + +sealed trait GeoprocessingException extends Exception +case class MissingTargetRasterException() extends GeoprocessingException() +case class MissingVectorCRSException() extends GeoprocessingException() +case class MissingVectorException() extends GeoprocessingException() +case class InvalidOperationException(val message: String) extends GeoprocessingException() +case class UnknownCRSException(val crs: String) extends GeoprocessingException() + +trait ErrorHandler { + val geoprocessingExceptionHandler = ExceptionHandler { + case InvalidOperationException(e) => { + println(s"Invalid operation type: $e") + complete(HttpResponse(BadRequest, entity = e)) + } + case MissingTargetRasterException() => { + println("Input error: Missing required targetRaster") + complete(HttpResponse(BadRequest, entity = "Missing required targetRaster")) + } + case MissingVectorCRSException() => { + println("Input error: Missing required vectorCRS") + complete(HttpResponse(BadRequest, entity = "Missing required vectorCRS")) + } + case MissingVectorException() => { + println(s"Input error: Missing required vector") + complete(HttpResponse(BadRequest, entity = "Missing required vector")) + } + case UnknownCRSException(crs) => { + println(s"Unknown CRS error: $crs") + complete(HttpResponse(BadRequest, entity = "Unknown CRS")) + } + case e: Exception => { + println(s"Exception: $e") + complete(HttpResponse(InternalServerError, entity = s"$e")) + } + } +} diff --git a/api/src/main/scala/Geoprocessing.scala b/api/src/main/scala/Geoprocessing.scala index 6972ddc..8d35dba 100644 --- a/api/src/main/scala/Geoprocessing.scala +++ b/api/src/main/scala/Geoprocessing.scala @@ -36,14 +36,14 @@ trait Geoprocessing extends Utils { * @param input The InputData * @return A histogram of results */ + @throws(classOf[MissingTargetRasterException]) def getRasterGroupedAverage(input: InputData): Future[ResultDouble] = { val aoi = createAOIFromInput(input) val futureLayers = cropRastersToAOI(input.rasters, input.zoom, aoi) val targetLayer = input.targetRaster match { case Some(targetRaster) => cropSingleRasterToAOI(targetRaster, input.zoom, aoi) - case None => - throw new Exception("Request data missing required 'targetRaster'.") + case None => throw new MissingTargetRasterException } val opts = getRasterizerOptions(input.pixelIsArea) @@ -62,6 +62,8 @@ trait Geoprocessing extends Utils { * @param input The InputData * @return A histogram of results */ + @throws(classOf[MissingVectorException]) + @throws(classOf[MissingVectorCRSException]) def getRasterLinesJoin(input: InputData): Future[ResultInt] = { val aoi = createAOIFromInput(input) val futureLayers = cropRastersToAOI(input.rasters, input.zoom, aoi) @@ -70,11 +72,9 @@ trait Geoprocessing extends Utils { input.vectorCRS match { case Some(crs) => createMultiLineFromInput(vector, crs, input.rasterCRS) - case None => - throw new Exception("Request data missing required 'vectorCRS'.") + case None => throw new MissingVectorCRSException } - case None => - throw new Exception("Request data missing required 'vector'.") + case None => throw new MissingVectorException } futureLayers.map { rasterLayers => diff --git a/api/src/main/scala/Utils.scala b/api/src/main/scala/Utils.scala index 730d177..9432de8 100644 --- a/api/src/main/scala/Utils.scala +++ b/api/src/main/scala/Utils.scala @@ -153,11 +153,12 @@ trait Utils { * @param crs The key (e.g. "input.rasterCRS") * @return A CRS */ + @throws(classOf[UnknownCRSException]) def getCRS(crs: String): CRS = crs match { case "LatLng" => LatLng case "WebMercator" => WebMercator case "ConusAlbers" => ConusAlbers - case s: String => throw new Exception(s"Unknown CRS: $s") + case s: String => throw new UnknownCRSException(s) } /** diff --git a/api/src/main/scala/WebServer.scala b/api/src/main/scala/WebServer.scala index 98448bb..1f58a2b 100644 --- a/api/src/main/scala/WebServer.scala +++ b/api/src/main/scala/WebServer.scala @@ -32,27 +32,32 @@ object PostRequestProtocol extends DefaultJsonProtocol { implicit val resultDoubleFormat = jsonFormat1(ResultDouble) } -object WebServer extends HttpApp with App with LazyLogging with Geoprocessing { +object WebServer extends HttpApp with App with LazyLogging with Geoprocessing with ErrorHandler { import PostRequestProtocol._ + @throws(classOf[InvalidOperationException]) def routes: Route = - get { - path("ping") { - complete("pong") - } - } ~ - post { - path("run") { - entity(as[PostRequest]) { data => - data.input.operationType match { - case "RasterGroupedCount" => - complete(getRasterGroupedCount(data.input)) - case "RasterGroupedAverage" => - complete(getRasterGroupedAverage(data.input)) - case "RasterLinesJoin" => - complete(getRasterLinesJoin(data.input)) - case _ => - throw new Exception(s"Unknown operationType: ${data.input.operationType}") + handleExceptions(geoprocessingExceptionHandler) { + get { + path("ping") { + complete("pong") + } + } ~ + post { + path("run") { + entity(as[PostRequest]) { data => + data.input.operationType match { + case "RasterGroupedCount" => + complete(getRasterGroupedCount(data.input)) + case "RasterGroupedAverage" => + complete(getRasterGroupedAverage(data.input)) + case "RasterLinesJoin" => + complete(getRasterLinesJoin(data.input)) + case _ => { + val message = s"Unknown operationType: ${data.input.operationType}" + throw new InvalidOperationException(message) + } + } } } }