Hi Kraythe,
Perhaps it helps to see a real world example that we've been working on -
with a good number of routes involved.
This is from our AkkaHttpServer class. It's job is to inject all the routes
(ordersV2, searchv3, searchTerms, persistence) which consist of around 6
actual endpoints per injected class - into the right point in the hierarchy
(Below the oAuth2 authenticator and any request/response loggers and
whatnot that you may need).
We define index.html and healthcheck route in this class since they are one
liners that live above oAuth2 security otherwise we would also inject them
independently.
Route indexRoute = get(() -> route(pathSingleSlash(() ->
getFromResource("web/index.html"))));
Route healthCheck = get(() -> path(PATH_HEALTH_CHECK, () ->
extractRequestContext(healthCheckHandler::handle)));
Route apis = route(
indexRoute,
healthCheck,
oauth2Authentication(
accessTokenVerifier,
route(
ordersV2,
searchV3,
searchTerms,
persistence
)
)
);
return logRequestResult(
this::requestMethodAsInfo,
this::rejectionsAsInfo,
() -> handleExceptions(
exceptionHandlerLogAndReturnInternalError(),
() -> handleRejections(
rejectionHandlerLogAndReturnNotFound(),
() -> apis
)
)
);
Note the *handleExceptions *and *handleRejections* methods. Basically if
your in the depths of a route (the bit supposed to handle the request) you
have 3 options.
1) Handle the request and reply with a HttpResponse.
return HttpResponse.create().withStatus(StatusCodes.CREATED).addHeader(
RawHeader.create(HttpHeaders.LOCATION, location)
)
2) Reject the request with an explicit Rejection
return reject(Rejections.authorizationFailed());
3) Throw an exception directly
throw new MyCustomException("Something went dodgy here!")
Now how you transpose those rejections or exceptions into a HttpResponse is
up to your generic rejection or exception handler right at the very top of
your routes. I've included our 'ErrorHandlingDirectives' class which we
simply extend (Instead of extending AllDirectives) wherever we need this:
public class ErrorHandlingDirectives extends AllDirectives {
private static final Logger LOG =
LoggerFactory.getLogger(ErrorHandlingDirectives.class);
public LogEntry requestMethodAsInfo(HttpRequest request, HttpResponse
response) {
String headers = toCensoredHeaderJson(request.getHeaders());
return LogEntry.create(
"Server has received a request\n"
+ request.method().name() + " " +
request.getUri().toString() + "\n"
+ headers + "\n"
+ "Server responded with a response\n"
+ response.status() + "\n"
+ "Content-Type: " +
response.entity().getContentType().toString() + "\n"
+ "Content-Length: " +
response.entity().getContentLengthOption().orElse(-1),
InfoLevel());
}
public LogEntry rejectionsAsInfo(HttpRequest request, List<Rejection>
rejections) {
String headers = toCensoredHeaderJson(request.getHeaders());
return LogEntry.create(
"Server has received a request\n"
+ request.method().name() + " " +
request.getUri().toString() + "\n"
+ headers + "\n"
+ "Server responded with a rejection\n"
+
rejections.stream().map(Rejection::toString).collect(Collectors.joining("\n")),
InfoLevel());
}
public ExceptionHandler exceptionHandlerLogAndReturnInternalError() {
return ExceptionHandler
.newBuilder()
.matchAny(throwable -> extractRequest(request -> {
LOG.warn("Error on route: " + request.method().value()
+ " " + request.getUri().toString() + " " + throwable.getMessage(),
throwable);
return complete(StatusCodes.INTERNAL_SERVER_ERROR);
})
).build();
}
public String toCensoredHeaderJson(Iterable<HttpHeader> headers) {
return StreamSupport
.stream(headers.spliterator(), false)
.map(header -> {
if (header instanceof Authorization) {
return header.name() + ": CENSORED";
}
return header.name() + ": " + header.value();
})
.collect(Collectors.joining("\n"));
}
}
So - yes you 'can' write 1 big file with a bazillion routes in it - or you
can do what most developers do eventually once things are working and split
it down into lots of individual classes with their own hierarchy. Have some
general rules for your approach (such as if someone throws a
BeanVerifiyException you return a particular type of HTTP status code) and
you'll soon be enjoying things again :)
As a general rule - whenever we reach the part of our route where we
actually plan to handle a request we generally spawn an actor to do the
job. For me it's good that the Actor speaks in terms of messages and failed
futures - and doesn't have to worry about 'Oh no I better not throw an
exception because I really need to return a HttpResponse of
INTERNAL_SERVER_ERROR'. This job can be done in your generic handlers :)
On Monday, 3 April 2017 02:09:32 UTC+1, kraythe wrote:
>
> I was really excited about akka-http as I would be able to unburden my
> code from the baggage of play and handle my server side as a pure akka
> actors app but unless I am much mistaken something is dreadfully amiss with
> the implementation.
>
> One of the main core features is the actor paradigm and the integration of
> a rich actor system. However, the preferred approach to akka-http seems to
> be a throwback to one file programming. The main reason it seems this way
> is the DSL. Take this example from a tutorial:
>
> path("bank" / IntNumber) { id =>
> get {
> complete {
> getById(id).map{result =>
> if(result.isDefined)
> HttpResponse(entity =write(result.get))
> else
> HttpResponse(entity ="This bank does not exist")
> }
>
>
> }
> }
> }~
> path("bank" / "update") {
> post {
> entity(as[String]) { bankJson =>
> complete {
> val bank =parse(bankJson).extract[Bank]
> update(bank).map{result => HttpResponse(entity ="Bank has
> been updated successfully")}
> }
> }
> }
> }
> }
> }
>
>
> Simple enough right? Too me I see the start of an anti-pattern but lets
> look further. It gets worse though, quickly, as shown in the akka-http
> documentation here
> <http://doc.akka.io/docs/akka-http/10.0.0/scala/http/routing-dsl/index.html#longer-example>.
>
> Still not bothered? The problem is that these examples are shallow and not
> rooted in the real world. In the bank application above would be hundreds
> of endpoints and each endpoint would have to validate data send by the
> user, check to see whether that data correct against the database and a
> dozen other things that would alter the nature of the return type to a bad
> request or internal error. The banking app would also have to log the
> problems so forensics can be done on malicious users. Just taking one route
> "deposit" would be several hundred lines of code INSIDE the route. However,
> it seems that there is no way to break off the route, offload it to another
> component (such as a Per Request Actor) and then continue the DSL where you
> left off. I had the chance to see for an app in another company that was
> asking my advice and their route is 12k lines long and at one point nested
> 30 levels deep.
>
> Now I know what you might say, "But Robert, you can break up the route
> into multiple files" which is true but something has to manually
> concatenate all of those routes together and they cant be done off of the
> main route. once you are in the routes DSL you are stuck there. Sure, you
> can call an actor with a future to do a completion but that actor itself
> might return data that requires a different kind of completion based upon
> certain criteria such as whether the user has had their account suspended.
> So if your completions are diverse, how do you break up the route?
>
> Now if someone has answers to these issues I would love to hear them but
> after researching I found that basically PRA's are deprecated in favor of a
> "convenient" DSL that entraps the user. For my purposes I opted to go with
> the low level API and factor off the route dispatching to a routing actor
> (yes, I know this is what the materializer does) and then just pull out
> route data the old fashioned way. My router, does path checking and then
> dispatches to another actor to handle that specific request and then sends
> the HttpResponse entity back to the sender which completes the ask and the
> route. My startup looks like this:
>
> val serverSource: Source[Http.IncomingConnection, Future[Http.
> ServerBinding]] =
> Http().bind(interface = "localhost", port = 8080)
> log.info("Server online at http://localhost:8080")
> val bindingFuture: Future[Http.ServerBinding] =
> serverSource.to(Sink.foreach { connection => // foreach materializes
> the source
> import akka.pattern.ask
> println("Accepted new connection from " + connection.remoteAddress)
> connection.handleWithAsyncHandler(request => (httpRouter ? request).
> mapTo[HttpResponse], parallelism = 4)
> }).run()
>
>
> A snippet of the router looks like this.
>
> class HttpRequestRouter extends Actor {
> protected val log = Logging(context.system, this)
>
>
> override def receive: Receive = {
> case request: HttpRequest =>
> val requestId = UUID.randomUUID()
> request match {
> case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
> notFound(requestId, request) // todo Implement this
> case HttpRequest(POST, Uri.Path("/hello"), _, _, _) =>
> invokeActor(classOf[HelloActor], requestId, request)
> case HttpRequest(GET, Uri.Path("/users"), _, _, _) =>
> invokeActor(classOf[ListUsersActor], requestId, request)
> case HttpRequest(GET, Uri.Path("/addUser"), _, _, _) =>
> invokeActor(classOf[AddUserActor], requestId, request)
> case uri =>
> notFound(requestId, request)
> }
> case msg => log.warning("Received unknown message: {}", msg)
> }
>
>
> private def invokeActor(actorType: Class[_], requestId: UUID, request:
> HttpRequest) = {
> context.actorOf(Props(actorType, sender(), requestId, request),
> requestId.toString)
> }}
>
> This allows me to fork off PRAs as needed but it kind of stinks in one way
> because there are a lot of tools in the DSL for unpacking entities and so
> on that I cant use, or rather if there is a way I and neither I nor anyone
> within the reach of google has figured it out.
>
> So what am I missing? Do people really love this monstrous DSL even though
> in a 100 endpoint system the thing will be gargantuan? Is there a means to
> fork off at any point in the DSL and then "reboot the stream"? It would be
> nice if some of the DSL tools could be invoked arbitrarily inside the PRAs
> on the request object like.
> class OrderPRA(replyTo: ActorRef, requestId: UUID, request: HttpRequest) {
>
> // ... code
> sender.tell(withRequest(request) {
> entity(as[Order]) { order =>
> complete {
> // ... write order to DB
> "Order received"
> }
> }
> }), self)
> }
>
>
> Opinions? Thoughts?
>
>
>
>
>
--
Notice: This email is confidential and may contain copyright material of
members of the Ocado Group. Opinions and views expressed in this message
may not necessarily reflect the opinions and views of the members of the
Ocado Group.
If you are not the intended recipient, please notify us immediately and
delete all copies of this message. Please note that it is your
responsibility to scan this message for viruses.
Fetch and Sizzle are trading names of Speciality Stores Limited and Fabled
is a trading name of Marie Claire Beauty Limited, both members of the Ocado
Group.
References to the “Ocado Group” are to Ocado Group plc (registered in
England and Wales with number 7098618) and its subsidiary undertakings (as
that expression is defined in the Companies Act 2006) from time to time.
The registered office of Ocado Group plc is Titan Court, 3 Bishops Square,
Hatfield Business Park, Hatfield, Herts. AL10 9NE.
--
>>>>>>>>>> Read the docs: http://akka.io/docs/
>>>>>>>>>> Check the FAQ:
>>>>>>>>>> http://doc.akka.io/docs/akka/current/additional/faq.html
>>>>>>>>>> Search the archives: https://groups.google.com/group/akka-user
---
You received this message because you are subscribed to the Google Groups "Akka
User List" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To post to this group, send email to [email protected].
Visit this group at https://groups.google.com/group/akka-user.
For more options, visit https://groups.google.com/d/optout.