diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java index 71fc1c95d6..1f9c3318d6 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/directives/ExecutionDirectivesTest.java @@ -56,4 +56,97 @@ public class ExecutionDirectivesTest extends JUnitRouteTest { .assertStatusCode(400) .assertEntity("Congratulations you provoked a division by zero!"); } + + @Test + public void testHandleMethodRejection() { + RejectionHandler rejectionHandler = + new RejectionHandler() { + @Override + public RouteResult handleMethodRejection(RequestContext ctx, HttpMethod supported) { + return ctx.complete( + HttpResponse.create() + .withStatus(400) + .withEntity("Whoopsie! Unsupported method. Supported would have been " + supported.value())); + } + }; + + TestRoute route = + testRoute( + handleRejections(rejectionHandler, + get(complete("Successful!")) + ) + ); + + route.run(HttpRequest.GET("/")) + .assertStatusCode(200) + .assertEntity("Successful!"); + + route.run(HttpRequest.POST("/")) + .assertStatusCode(400) + .assertEntity("Whoopsie! Unsupported method. Supported would have been GET"); + } + + public static final class TooManyRequestsRejection extends CustomRejection { + final public String message; + TooManyRequestsRejection(String message) { + this.message = message; + } + } + + private static Handler testHandler = + new Handler() { + @Override + public RouteResult apply(RequestContext ctx) { + if (ctx.request().getUri().path().startsWith("/test")) + return ctx.complete("Successful!"); + else + return ctx.reject(new TooManyRequestsRejection("Too many requests for busy path!")); + } + }; + + @Test + public void testHandleCustomRejection() { + RejectionHandler rejectionHandler = + new RejectionHandler() { + @Override + public RouteResult handleCustomRejection(RequestContext ctx, CustomRejection rejection) { + if (rejection instanceof TooManyRequestsRejection) { + TooManyRequestsRejection rej = (TooManyRequestsRejection) rejection; + HttpResponse response = + HttpResponse.create() + .withStatus(StatusCodes.TOO_MANY_REQUESTS) + .withEntity(rej.message); + return ctx.complete(response); + } else + return passRejection(); + } + }; + + testRouteWithHandler(handleRejections(rejectionHandler, handleWith(testHandler))); + } + @Test + public void testHandleCustomRejectionByClass() { + Handler1 rejectionHandler = + new Handler1() { + public RouteResult apply(RequestContext ctx, TooManyRequestsRejection rej) { + HttpResponse response = + HttpResponse.create() + .withStatus(StatusCodes.TOO_MANY_REQUESTS) + .withEntity(rej.message); + return ctx.complete(response); + } + }; + testRouteWithHandler(handleRejections(TooManyRequestsRejection.class, rejectionHandler, handleWith(testHandler))); + } + + private void testRouteWithHandler(Route innerRoute) { + TestRoute route = testRoute(innerRoute); + + route.run(HttpRequest.GET("/test")) + .assertStatusCode(200); + + route.run(HttpRequest.GET("/other")) + .assertStatusCode(429) + .assertEntity("Too many requests for busy path!"); + } } diff --git a/akka-http/src/main/scala/akka/http/impl/server/CustomRejectionWrapper.scala b/akka-http/src/main/scala/akka/http/impl/server/CustomRejectionWrapper.scala new file mode 100644 index 0000000000..70dce92ca4 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/impl/server/CustomRejectionWrapper.scala @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.impl.server + +import akka.http.javadsl.server.CustomRejection +import akka.http.scaladsl.server.Rejection + +/** + * A wrapper that packs a Java custom rejection into a Scala Rejection. + * + * INTERNAL API + */ +private[http] case class CustomRejectionWrapper(customRejection: CustomRejection) extends Rejection diff --git a/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala b/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala new file mode 100644 index 0000000000..3e76865158 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.impl.server + +import scala.collection.immutable + +import akka.http.javadsl.server +import akka.http.javadsl.server.RouteResult +import akka.http.scaladsl.server._ + +import akka.http.impl.util.JavaMapping.Implicits._ + +/** + * INTERNAL API + */ +private[http] class RejectionHandlerWrapper(javaHandler: server.RejectionHandler) extends RejectionHandler { + def apply(rejs: immutable.Seq[Rejection]): Option[Route] = Some { scalaCtx ⇒ + val ctx = new RequestContextImpl(scalaCtx) + + import javaHandler._ + def handle(): RouteResult = + if (rejs.isEmpty) handleEmptyRejection(ctx) + else rejs.head match { + case MethodRejection(supported) ⇒ + handleMethodRejection(ctx, supported.asJava) + case SchemeRejection(supported) ⇒ + handleSchemeRejection(ctx, supported) + case MissingQueryParamRejection(parameterName) ⇒ + handleMissingQueryParamRejection(ctx, parameterName) + case MalformedQueryParamRejection(parameterName, errorMsg, cause) ⇒ + handleMalformedQueryParamRejection(ctx, parameterName, errorMsg, cause.orNull) + case MissingFormFieldRejection(fieldName) ⇒ + handleMissingFormFieldRejection(ctx, fieldName) + case MalformedFormFieldRejection(fieldName, errorMsg, cause) ⇒ + handleMalformedFormFieldRejection(ctx, fieldName, errorMsg, cause.orNull) + case MissingHeaderRejection(headerName) ⇒ + handleMissingHeaderRejection(ctx, headerName) + case MalformedHeaderRejection(headerName, errorMsg, cause) ⇒ + handleMalformedHeaderRejection(ctx, headerName, errorMsg, cause.orNull) + case UnsupportedRequestContentTypeRejection(supported) ⇒ + handleUnsupportedRequestContentTypeRejection(ctx, supported.toList.toSeq.asJava) + case UnsupportedRequestEncodingRejection(supported) ⇒ + handleUnsupportedRequestEncodingRejection(ctx, supported.asJava) + case UnsatisfiableRangeRejection(unsatisfiableRanges, actualEntityLength) ⇒ + handleUnsatisfiableRangeRejection(ctx, unsatisfiableRanges.asJava, actualEntityLength) + case TooManyRangesRejection(maxRanges) ⇒ + handleTooManyRangesRejection(ctx, maxRanges) + case MalformedRequestContentRejection(message, cause) ⇒ + handleMalformedRequestContentRejection(ctx, message, cause.orNull) + case RequestEntityExpectedRejection ⇒ + handleRequestEntityExpectedRejection(ctx) + case UnacceptedResponseContentTypeRejection(supported) ⇒ + handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.toSeq.asJava) + case UnacceptedResponseEncodingRejection(supported) ⇒ + handleUnacceptedResponseEncodingRejection(ctx, supported.toList.toSeq.asJava) + case AuthenticationFailedRejection(cause, challenge) ⇒ + handleAuthenticationFailedRejection(ctx, cause == AuthenticationFailedRejection.CredentialsMissing, challenge) + case AuthorizationFailedRejection ⇒ + handleAuthorizationFailedRejection(ctx) + case MissingCookieRejection(cookieName) ⇒ + handleMissingCookieRejection(ctx, cookieName) + case ExpectedWebsocketRequestRejection ⇒ + handleExpectedWebsocketRequestRejection(ctx) + case UnsupportedWebsocketSubprotocolRejection(supportedProtocol) ⇒ + handleUnsupportedWebsocketSubprotocolRejection(ctx, supportedProtocol) + case ValidationRejection(message, cause) ⇒ + handleValidationRejection(ctx, message, cause.orNull) + + case CustomRejectionWrapper(custom) ⇒ handleCustomRejection(ctx, custom) + case o ⇒ handleCustomScalaRejection(ctx, o) + } + + handle() match { + case r: RouteResultImpl ⇒ r.underlying + case PassRejectionRouteResult ⇒ scalaCtx.reject(rejs: _*) + } + } +} diff --git a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala index 294b03cfa8..2e2dad778c 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala @@ -46,6 +46,8 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex def notFound(): RouteResult = underlying.reject() + def reject(customRejection: CustomRejection): RouteResult = underlying.reject(CustomRejectionWrapper(customRejection)) + def executionContext(): ExecutionContext = underlying.executionContext def materializer(): Materializer = underlying.materializer } diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala index 7bf603e686..4519a88a3f 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteImplementation.scala @@ -114,6 +114,7 @@ private[http] object RouteImplementation extends Directives with server.RouteCon } handleExceptions(pf) + case HandleRejections(handler) ⇒ handleRejections(new RejectionHandlerWrapper(handler)) case Validated(isValid, errorMsg) ⇒ validate(isValid, errorMsg) case RangeSupport() ⇒ withRangeSupport case SetCookie(cookie) ⇒ setCookie(cookie.asScala) diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteResultImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteResultImpl.scala index 2f94d482ab..0bc2e02d04 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteResultImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteResultImpl.scala @@ -13,10 +13,18 @@ import akka.http.scaladsl.{ server ⇒ ss } * INTERNAL API */ private[http] class RouteResultImpl(val underlying: Future[ss.RouteResult]) extends js.RouteResult + /** * INTERNAL API */ private[http] object RouteResultImpl { implicit def autoConvert(result: Future[ss.RouteResult]): js.RouteResult = new RouteResultImpl(result) -} \ No newline at end of file +} + +/** + * Internal result that flags that a rejection was not handled by a rejection handler. + * + * INTERNAL API + */ +private[http] case object PassRejectionRouteResult extends js.RouteResult \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala index b2a7fe90fb..9981538080 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RouteStructure.scala @@ -70,6 +70,7 @@ private[http] object RouteStructure { case class Validated(isValid: Boolean, errorMsg: String)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute case class HandleExceptions(handler: ExceptionHandler)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute + case class HandleRejections(handler: RejectionHandler)(val innerRoute: Route, val moreInnerRoutes: immutable.Seq[Route]) extends DirectiveRoute sealed abstract class HostFilter extends DirectiveRoute { def filter(hostName: String): Boolean diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/CustomRejection.scala b/akka-http/src/main/scala/akka/http/javadsl/server/CustomRejection.scala new file mode 100644 index 0000000000..05e64ca6dc --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/CustomRejection.scala @@ -0,0 +1,10 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server + +/** + * Base class for application defined rejections. + */ +abstract class CustomRejection diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala new file mode 100644 index 0000000000..975efaa877 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server + +import java.{ lang ⇒ jl } + +import akka.http.impl.server.PassRejectionRouteResult +import akka.http.javadsl.model.{ ContentType, ContentTypeRange, HttpMethod } +import akka.http.javadsl.model.headers.{ HttpChallenge, ByteRange, HttpEncoding } +import akka.http.scaladsl.server.Rejection + +/** + * The base class for defining a RejectionHandler to be used with the `handleRejection` directive. + * Override one of the handler methods to define a route to be used in case the inner route + * rejects a request with the given rejection. + * + * Default implementations pass the rejection to outer handlers. + */ +abstract class RejectionHandler { + /** + * Callback called to handle the empty rejection which represents the + * "Not Found" condition. + */ + def handleEmptyRejection(ctx: RequestContext): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by method filters. + * Signals that the request was rejected because the HTTP method is unsupported. + * + * The default implementation does not handle the rejection. + */ + def handleMethodRejection(ctx: RequestContext, supported: HttpMethod): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by scheme filters. + * Signals that the request was rejected because the Uri scheme is unsupported. + */ + def handleSchemeRejection(ctx: RequestContext, supported: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by parameter filters. + * Signals that the request was rejected because a query parameter was not found. + */ + def handleMissingQueryParamRejection(ctx: RequestContext, parameterName: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by parameter filters. + * Signals that the request was rejected because a query parameter could not be interpreted. + */ + def handleMalformedQueryParamRejection(ctx: RequestContext, parameterName: String, errorMsg: String, + cause: Throwable): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by form field filters. + * Signals that the request was rejected because a form field was not found. + */ + def handleMissingFormFieldRejection(ctx: RequestContext, fieldName: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by form field filters. + * Signals that the request was rejected because a form field could not be interpreted. + */ + def handleMalformedFormFieldRejection(ctx: RequestContext, fieldName: String, errorMsg: String, + cause: Throwable): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by header directives. + * Signals that the request was rejected because a required header could not be found. + */ + def handleMissingHeaderRejection(ctx: RequestContext, headerName: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by header directives. + * Signals that the request was rejected because a header value is malformed. + */ + def handleMalformedHeaderRejection(ctx: RequestContext, headerName: String, errorMsg: String, + cause: Throwable): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by unmarshallers. + * Signals that the request was rejected because the requests content-type is unsupported. + */ + def handleUnsupportedRequestContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[ContentTypeRange]): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by decoding filters. + * Signals that the request was rejected because the requests content encoding is unsupported. + */ + def handleUnsupportedRequestEncodingRejection(ctx: RequestContext, supported: HttpEncoding): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by range directives. + * Signals that the request was rejected because the requests contains only unsatisfiable ByteRanges. + * The actualEntityLength gives the client a hint to create satisfiable ByteRanges. + */ + def handleUnsatisfiableRangeRejection(ctx: RequestContext, unsatisfiableRanges: jl.Iterable[ByteRange], actualEntityLength: Long): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by range directives. + * Signals that the request contains too many ranges. An irregular high number of ranges + * indicates a broken client or a denial of service attack. + */ + def handleTooManyRangesRejection(ctx: RequestContext, maxRanges: Int): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by unmarshallers. + * Signals that the request was rejected because unmarshalling failed with an error that wasn't + * an `IllegalArgumentException`. Usually that means that the request content was not of the expected format. + * Note that semantic issues with the request content (e.g. because some parameter was out of range) + * will usually trigger a `ValidationRejection` instead. + */ + def handleMalformedRequestContentRejection(ctx: RequestContext, message: String, cause: Throwable): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by unmarshallers. + * Signals that the request was rejected because an message body entity was expected but not supplied. + */ + def handleRequestEntityExpectedRejection(ctx: RequestContext): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by marshallers. + * Signals that the request was rejected because the service is not capable of producing a response entity whose + * content type is accepted by the client + */ + def handleUnacceptedResponseContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[ContentType]): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by encoding filters. + * Signals that the request was rejected because the service is not capable of producing a response entity whose + * content encoding is accepted by the client + */ + def handleUnacceptedResponseEncodingRejection(ctx: RequestContext, supported: jl.Iterable[HttpEncoding]): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by an [[akka.http.scaladsl.server.authentication.HttpAuthenticator]]. + * Signals that the request was rejected because the user could not be authenticated. The reason for the rejection is + * specified in the cause. + * + * If credentialsMissing is false, existing credentials were rejected. + */ + def handleAuthenticationFailedRejection(ctx: RequestContext, credentialsMissing: Boolean, challenge: HttpChallenge): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by the 'authorize' directive. + * Signals that the request was rejected because the user is not authorized. + */ + def handleAuthorizationFailedRejection(ctx: RequestContext): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by the `cookie` directive. + * Signals that the request was rejected because a cookie was not found. + */ + def handleMissingCookieRejection(ctx: RequestContext, cookieName: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created when a websocket request was expected but none was found. + */ + def handleExpectedWebsocketRequestRejection(ctx: RequestContext): RouteResult = passRejection() + + /** + * Callback called to handle rejection created when a websocket request was not handled because none + * of the given subprotocols was supported. + */ + def handleUnsupportedWebsocketSubprotocolRejection(ctx: RequestContext, supportedProtocol: String): RouteResult = passRejection() + + /** + * Callback called to handle rejection created by the `validation` directive as well as for `IllegalArgumentExceptions` + * thrown by domain model constructors (e.g. via `require`). + * It signals that an expected value was semantically invalid. + */ + def handleValidationRejection(ctx: RequestContext, message: String, cause: Throwable): RouteResult = passRejection() + + /** + * Callback called to handle any custom rejection defined by the application. + */ + def handleCustomRejection(ctx: RequestContext, rejection: CustomRejection): RouteResult = passRejection() + + /** + * Callback called to handle any other Scala rejection that is not covered by this class. + */ + def handleCustomScalaRejection(ctx: RequestContext, rejection: Rejection): RouteResult = passRejection() + + /** + * Use the RouteResult returned by this method in handler implementations to signal that a rejection was not handled + * and should be passed to an outer rejection handler. + */ + protected final def passRejection(): RouteResult = PassRejectionRouteResult +} diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala index f2836b72ff..d62d220fed 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala @@ -73,5 +73,8 @@ trait RequestContext { */ def notFound(): RouteResult - // FIXME: provide proper support for rejections, see #16438 + /** + * Reject this request with an application-defined CustomRejection. + */ + def reject(customRejection: CustomRejection): RouteResult } \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala index b6d18757cc..144704b526 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/ExecutionDirectives.scala @@ -8,6 +8,7 @@ package directives import akka.http.impl.server.RouteStructure import scala.annotation.varargs +import scala.reflect.ClassTag abstract class ExecutionDirectives extends CookieDirectives { /** @@ -16,4 +17,25 @@ abstract class ExecutionDirectives extends CookieDirectives { @varargs def handleExceptions(handler: ExceptionHandler, innerRoute: Route, moreInnerRoutes: Route*): Route = RouteStructure.HandleExceptions(handler)(innerRoute, moreInnerRoutes.toList) + + /** + * Handles rejections in the inner routes using the specified handler. + */ + @varargs + def handleRejections(handler: RejectionHandler, innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.HandleRejections(handler)(innerRoute, moreInnerRoutes.toList) + + /** + * Handles rejections of the given type in the inner routes using the specified handler. + */ + @varargs + def handleRejections[T](tClass: Class[T], handler: Handler1[T], innerRoute: Route, moreInnerRoutes: Route*): Route = + RouteStructure.HandleRejections(new RejectionHandler { + implicit def tTag: ClassTag[T] = ClassTag(tClass) + override def handleCustomRejection(ctx: RequestContext, rejection: CustomRejection): RouteResult = + rejection match { + case t: T ⇒ handler.apply(ctx, t) + case _ ⇒ passRejection() + } + })(innerRoute, moreInnerRoutes.toList) }