+htp #16438 add Java-side rejection handling support

This commit is contained in:
Johannes Rudolph 2015-09-09 10:31:00 +02:00
parent 96ef8875c8
commit db1be86b02
11 changed files with 427 additions and 2 deletions

View file

@ -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<TooManyRequestsRejection> rejectionHandler =
new Handler1<TooManyRequestsRejection>() {
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!");
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
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

View file

@ -0,0 +1,80 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
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: _*)
}
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)
}
}
/**
* Internal result that flags that a rejection was not handled by a rejection handler.
*
* INTERNAL API
*/
private[http] case object PassRejectionRouteResult extends js.RouteResult

View file

@ -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

View file

@ -0,0 +1,10 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.javadsl.server
/**
* Base class for application defined rejections.
*/
abstract class CustomRejection

View file

@ -0,0 +1,190 @@
/*
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
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
}

View file

@ -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
}

View file

@ -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)
}