High-level Server-Side API
In addition to the Low-Level Server-Side API Akka HTTP provides a very flexible “Routing DSL” for elegantly defining RESTful web services. It picks up where the low-level API leaves off and offers much of the higher-level functionality of typical web servers or frameworks, like deconstruction of URIs, content negotiation or static content serving.
To use the high-level API you need to add a dependency to the akka-http
module.
Minimal Example
This is a complete, very basic Akka HTTP application relying on the Routing DSL:
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.http.javadsl.ConnectHttp;
import akka.http.javadsl.Http;
import akka.http.javadsl.ServerBinding;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.server.AllDirectives;
import akka.http.javadsl.server.Route;
import akka.stream.ActorMaterializer;
import akka.stream.javadsl.Flow;
import java.util.concurrent.CompletionStage;
public class HttpServerMinimalExampleTest extends AllDirectives {
public static void main(String[] args) throws Exception {
// boot up server using the route as defined below
ActorSystem system = ActorSystem.create("routes");
final Http http = Http.get(system);
final ActorMaterializer materializer = ActorMaterializer.create(system);
//In order to access all directives we need an instance where the routes are define.
HttpServerMinimalExampleTest app = new HttpServerMinimalExampleTest();
final Flow<HttpRequest, HttpResponse, NotUsed> routeFlow = app.createRoute().flow(system, materializer);
final CompletionStage<ServerBinding> binding = http.bindAndHandle(routeFlow,
ConnectHttp.toHost("localhost", 8080), materializer);
System.out.println("Server online at http://localhost:8080/\nPress RETURN to stop...");
System.in.read(); // let it run until user presses return
binding
.thenCompose(ServerBinding::unbind) // trigger unbinding from the port
.thenAccept(unbound -> system.terminate()); // and shutdown when done
}
private Route createRoute() {
return route(
path("hello", () ->
get(() ->
complete("<h1>Say hello to akka-http</h1>"))));
}
}
It starts an HTTP Server on localhost and replies to GET requests to /hello
with a simple response.
The following example uses an experimental feature and its API is subjected to change in future releases of Akka HTTP. For further information about this marker, see The @DoNotInherit and @ApiMayChange markers in the Akka documentation.
To help start a server Akka HTTP provides an experimental helper class called HttpApp
. This is the same example as before rewritten using HttpApp
:
// Server definition
class MinimalHttpApp extends HttpApp {
@Override
protected Route routes() {
return path("hello", () ->
get(() ->
complete("<h1>Say hello to akka-http</h1>")
)
);
}
}
// Starting the server
final MinimalHttpApp myServer = new MinimalHttpApp();
myServer.startServer("localhost", 8080);
See HttpApp Bootstrap for more details about setting up a server using this approach.
Handling HTTP Server failures in the High-Level API
There are various situations when failure may occur while initialising or running an Akka HTTP server. Akka by default will log all these failures, however sometimes one may want to react to failures in addition to them just being logged, for example by shutting down the actor system, or notifying some external monitoring end-point explicitly.
Bind failures
For example the server might be unable to bind to the given port. For example when the port is already taken by another application, or if the port is privileged (i.e. only usable by root
). In this case the “binding future” will fail immediately, and we can react to it by listening on the CompletionStage’s completion:
import akka.NotUsed;
import akka.actor.ActorSystem;
import akka.http.javadsl.ConnectHttp;
import akka.http.javadsl.ServerBinding;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.HttpResponse;
import akka.http.javadsl.server.Route;
import akka.http.javadsl.Http;
import akka.stream.ActorMaterializer;
import akka.stream.javadsl.Flow;
import java.io.IOException;
import java.util.concurrent.CompletionStage;
public class HighLevelServerBindFailureExample {
public static void main(String[] args) throws IOException {
// boot up server using the route as defined below
final ActorSystem system = ActorSystem.create();
final ActorMaterializer materializer = ActorMaterializer.create(system);
final HighLevelServerExample app = new HighLevelServerExample();
final Route route = app.createRoute();
final Flow<HttpRequest, HttpResponse, NotUsed> handler = route.flow(system, materializer);
final CompletionStage<ServerBinding> binding = Http.get(system).bindAndHandle(handler, ConnectHttp.toHost("127.0.0.1", 8080), materializer);
binding.exceptionally(failure -> {
System.err.println("Something very bad happened! " + failure.getMessage());
system.terminate();
return null;
});
system.terminate();
}
}
For a more low-level overview of the kinds of failures that can happen and also more fine-grained control over them refer to the Handling HTTP Server failures in the Low-Level API documentation.
Failures and exceptions inside the Routing DSL
Exception handling within the Routing DSL is done by providing ExceptionHandler
s which are documented in-depth in the Exception Handling section of the documentation. You can use them to transform exceptions into HttpResponse
s with appropriate error codes and human-readable failure descriptions.
File uploads
For high level directives to handle uploads see the FileUploadDirectives.
Handling a simple file upload from for example a browser form with a file input can be done by accepting a Multipart.FormData entity, note that the body parts are Source rather than all available right away, and so is the individual body part payload so you will need to consume those streams both for the file and for the form fields.
Here is a simple example which just dumps the uploaded file into a temporary file on disk, collects some form fields and saves an entry to a fictive database:
path("video", () ->
entity(Unmarshaller.entityToMultipartFormData(), formData -> {
// collect all parts of the multipart as it arrives into a map
final CompletionStage<Map<String, Object>> allParts =
formData.getParts().mapAsync(1, bodyPart -> {
if ("file".equals(bodyPart.getName())) {
// stream into a file as the chunks of it arrives and return a CompletionStage
// file to where it got stored
final File file = File.createTempFile("upload", "tmp");
return bodyPart.getEntity().getDataBytes()
.runWith(FileIO.toPath(file.toPath()), materializer)
.thenApply(ignore ->
new Pair<String, Object>(bodyPart.getName(), file)
);
} else {
// collect form field values
return bodyPart.toStrict(2 * 1000, materializer)
.thenApply(strict ->
new Pair<String, Object>(bodyPart.getName(),
strict.getEntity().getData().utf8String())
);
}
}).runFold(new HashMap<String, Object>(), (acc, pair) -> {
acc.put(pair.first(), pair.second());
return acc;
}, materializer);
// simulate a DB call
final CompletionStage<Void> done = allParts.thenCompose(map ->
// You would have some better validation/unmarshalling here
DB.create((File) map.get("file"),
(String) map.get("title"),
(String) map.get("author")
));
// when processing have finished create a response for the user
return onSuccess(() -> allParts, x -> complete("ok!"));
})
);
You can transform the uploaded files as they arrive rather than storing then in a temporary file as in the previous example. In this example we accept any number of .csv
files, parse those into lines and split each line before we send it to an actor for further processing:
Route csvUploads() {
final Flow<ByteString, ByteString, NotUsed> splitLines =
Framing.delimiter(ByteString.fromString("\n"), 256);
return path(segment("metadata").slash(longSegment()), id ->
entity(Unmarshaller.entityToMultipartFormData(), formData -> {
final CompletionStage<Done> done = formData.getParts().mapAsync(1, bodyPart ->
bodyPart.getFilename().filter(name -> name.endsWith(".csv")).map(ignored ->
bodyPart.getEntity().getDataBytes()
.via(splitLines)
.map(bs -> bs.utf8String().split(","))
.runForeach(csv ->
metadataActor.tell(new Entry(id, csv), ActorRef.noSender()),
materializer)
).orElseGet(() ->
// in case the uploaded file is not a CSV
CompletableFuture.completedFuture(Done.getInstance()))
).runWith(Sink.ignore(), materializer);
// when processing have finished create a response for the user
return onComplete(() -> done, ignored -> complete("ok!"));
})
);
}