How do I access the full path of a request in Akka HTTP request? - scala

In some contexts I can match on the remaining path using a PathDirective to get the information I need about the path. For example, when route below is directly bound and handled by Akka HTTP, every request will echo back the requested path as desired.
val route =
path(Remaining) { path =>
complete(path)
}
However, when the above route is combined elsewhere in the application, the path variable above may only hold part of the requested path not giving the desired results.
For example if the actual bound route is be,
val actualRoute = pathPrefix("echo") { route }
The "echo/" part of the overall path will be missing from the response given to the user.
How can the complete path be reliably accessed?

Directives extractMatchedPath and extractUnmatchedPath let you access the path without matching the path like you path directive does above. These two can be combined to construct the full path:
val route =
extractMatchedPath { matched =>
extractUnmatchedPath { unmatched =>
complete((matched.dropChars(1) ++ unmatched).toString)
}
}
However it is probably cleaner just to extract the Path from the URI directly:
val route =
extractUri { uri =>
complete(uri.toRelative.path.dropChars(1).toString)
}
Note that in both cases we needed to call .dropChars(1) to drop the initial forward slash and be consistent with the output you got using the path directive.

Related

vertx how to reroute with query params

Due to some url versioning, we try to map multiple paths to the same handler.
I tried to achieve this via rerouting but the query parameters get lost in the process.
// reroute if the path contains apiv3 / api v3
router.route("/apiv3/*").handler( context -> {
String path = context.request().path();
path = path.replace("apiv3/", "");
LOG.info("Path changed to {}", path);
context.reroute(path);
});
What is the most elegant way around this problem?
There are some discussions on google groups but surprisingly nothing quick and simple to implement.
The reroute documentation says:
It should be clear that reroute works on paths, so if you need to
preserve and or add state across reroutes, one should use the
RoutingContext object.
So you could create a global catch-all route that stores any query param in the RoutingContext:
router.route().handler(ctx -> {
ctx.put("queryParams", ctx.queryParams());
ctx.next();
});
Then your apiv3 catch-all route:
router.route("/apiv3/*").handler( context -> {
String path = context.request().path();
path = path.replace("apiv3/", "");
LOG.info("Path changed to {}", path);
context.reroute(path);
});
Finally an actual route handler:
router.get("/products").handler(rc -> {
MultiMap queryParams = rc.get("queryParams");
// handle request
});

Why is Akka-Http Routing going wrong here?

So basically, I'm writing some code that will make it possible for users to upload a file to a server. I've already succeeded in uploading a file via an HTML form (with MultiPart.FormData), but when I try 'curl -X POST -F file="filepath" localhost:8080/upload', I get a '404 not found' message.
I already read the documentation about Akka, but I just can't get to know why it works in one way and not in the other. What am I doing wrong here?
val route =
post {
path("upload") {
fileUpload("file") {
case (metadata, byteSource) =>
val sink = FileIO.toPath(Paths.get("path of the image") resolve metadata.fileName)
val writeResult = byteSource.runWith(sink)
onSuccess(writeResult) { _ =>
complete("file got uploaded")
}
}
}
} ~
complete("404 not found")
You can see in path directive source that it accepts a path prefix with the path end. So if you use path("upload") it will accept only paths ended with /upload/ but won't accept paths ended with /upload (without path end symbol /).
If you wont to use both /upload/ and /upload paths, you should use
pathPrefix("upload") ~ pathEndOrSingleSlash
Also, you can use ignoreTrailingSlash directive.

Treat index.html as default file in Akka HTTP's getFromDirectory

Assuming I have a folder foo with an index.html file in it and the following minimal (but most likely not functional) server code for Akka HTTP below:
object Server extends App {
val route: Route =
pathPrefix("foo") {
getFromDirectory("foo")
}
Http().bindAndHandle(route, "0.0.0.0", 8080)
}
index.html will correctly be served if I open http://localhost:8080/foo/index.html in a browser, but not if I open http://localhost:8080/foo or http://localhost:8080/foo/.
If this is even possible, how can I set up my Akka HTTP routes to serve index.html files within that location by default?
I know I could do the following:
val route: Route =
path("foo") {
getFromFile("foo/index.html")
} ~
pathPrefix("foo") {
getFromDirectory("foo")
}
But:
This only makes http://localhost:8080/foo work, not http://localhost:8080/foo/
This is very ad-hoc and does not apply globally: if I have a foo/bar/index.html file, the problem will be the same.
You can create the Route you are looking for by using the pathEndOrSingleSlash Directive:
val route =
pathPrefix("foo") {
pathEndOrSingleSlash {
getFromFile("foo/index.html")
} ~
getFromDirectory("foo")
}
This Route will match at the end of the path and feed up the index.html, or if the end doesn't match it will call getFromDirectory instead.
If you want to make this "global" you can make a function out of it:
def routeAsDir[T](pathMatcher : PathMatcher[T], dir : String) : Route =
pathPrefix(pathMatcher) {
pathEndOrSingleSlash {
getFromFile(dir + "/index.html")
} ~
getFromDirectory(dir)
}
This can then be called with
val barRoute = routeAsDir("foo" / "bar", "foo/bar")
Functional Akka Http
Side note: your code is completely functional. The elegance of the Directives DSL can be a bit misleading and convince you that you've accidentally strayed back into imperative programming style. But each of the Directives is simply a function; can't get more "functional" than that...

Require header across routes without giving up 404

I’d like to require a header in my akka-http routes and can do so via
val route = headerValueByName("foo") { foo =>
pathPrefix("path") {
get {
...
} ~ ...
}
}
However, now any requests that don't match a path will get rejected with 400 (missing header) and not 404.
Is there a neat way to get around this without repeatedly moving headerValueByName after the path matchers?
That is, is there a way to only apply an outer directive ( headerValueByName) and its rejections if the inner path and method matchers are successful?
You don't specify what you want to do in case the header is not specified, so I'll asume you want to return 400 (Bad request).
A possible solution is to use the optionalHeaderValueByName directive and then complete the request with the specified error, for example:
val route = optionalHeaderValueByName("foo") { optionalHeader =>
optionalHeader map { header =>
// rest of your routes
} getOrElse complete(StatusCodes.BadRequest)
}

Serving static /public/ file from Play 2 Scala controller

What is the preferred method to serve a static file from a Play Framework 2 Scala controller?
The file is bundled with my application, so it's not possible to hardcode a filesystem absolute /path/to/the/file, because its location depends on where the Play app happens to be installeld.
The file is placed in the public/ dir, but not in app/assets/, because I don't want Play to compile it.
(The reason I don't simply add a route to that file, is that one needs to login before accessing that file, otherwise it's of no use.)
Here is what I've done so far, but this breaks on my production server.
object Application ...
def viewAdminPage = Action ... {
... authorization ...
val adminPageFile = Play.getFile("/public/admin/index.html")
Ok.sendFile(adminPageFile, inline = true)
}
And in my routes file, I have this line:
GET /-/admin/ controllers.Application.viewAdminPage
The problem is that on my production server, this error happens:
FileNotFoundException: app1/public/admin/index.html
Is there some other method, rather than Play.getFile and OK.sendFile, to specify which file to serve? That never breaks in production?
(My app is installed in /some-dir/app1/ and I start it from /some-dir/ (without app1/) — perhaps everything would work if I instead started the app from /some-dir/app1/. But I'd like to know how one "should" do, to serve a static file from inside a controller? So that everything always works also on the production servers, regardless of from where I happen to start the application)
Check Streaming HTTP responses doc
def index = Action {
Ok.sendFile(
content = new java.io.File("/tmp/fileToServe.pdf"),
fileName = _ => "termsOfService.pdf"
)
}
You can add some random string to the fileName (individual for each logged user) to avoid sharing download link between authenticated and non-authinticated users and also make advanced download stats.
I did this: (but see the Update below!)
val fileUrl: java.net.URL = this.getClass().getResource("/public/admin/file.html")
val file = new java.io.File(adminPageUrl.toURI())
Ok.sendFile(file, inline = true)
(this is the controller, which is (and must be) located in the same package as the file that's being served.)
Here is a related question: open resource with relative path in java
Update
Accessing the file via an URI causes an error: IllegalArgumentException: URI is not hierarchical, if the file is then located inside a JAR, which is the case if you run Play like so: play stage and then target/start.
So instead I read the file as a stream, converted it to a String, and sent that string as HTML:
val adminPageFileString: String = {
// In prod builds, the file is embedded in a JAR, and accessing it via
// an URI causes an IllegalArgumentException: "URI is not hierarchical".
// So use a stream instead.
val adminPageStream: java.io.InputStream =
this.getClass().getResourceAsStream("/public/admin/index.html")
io.Source.fromInputStream(adminPageStream).mkString("")
}
...
return Ok(adminPageFileString) as HTML
Play has a built-in method for this:
Ok.sendResource("public/admin/file.html", classLoader)
You can obtain a classloader from an injected Environment with environment.classLoader or from this.getClass.getClassLoader.
The manual approach for this is the following:
val url = Play.resource(file)
url.map { url =>
val stream = url.openStream()
val length = stream.available
val resourceData = Enumerator.fromStream(stream)
val headers = Map(
CONTENT_LENGTH -> length.toString,
CONTENT_TYPE -> MimeTypes.forFileName(file).getOrElse(BINARY),
CONTENT_DISPOSITION -> s"""attachment; filename="$name"""")
SimpleResult(
header = ResponseHeader(OK, headers),
body = resourceData)
The equivalent using the assets controller is this:
val name = "someName.ext"
val response = Assets.at("/public", name)(request)
response
.withHeaders(CONTENT_DISPOSITION -> s"""attachment; filename="$name"""")
Another variant, without using a String, but by streaming the file content:
def myStaticRessource() = Action { implicit request =>
val contentStream = this.getClass.getResourceAsStream("/public/content.html")
Ok.chunked(Enumerator.fromStream(contentStream)).as(HTML)
}