+++ template = "article.html" title = "Serving static files with Ktor" date = 2022-09-03T19:00:00+10:00 description = "A guide to serving static files with Ktor, including configuration for default files, HTML minification, and proper MIME types." [taxonomies] tags = ["kotlin", "ktor", "web"] +++ Serving static files with [Ktor](https://ktor.io) for a static website seems easy enough: ```kotlin fun main() { embeddedServer(Netty, port = 8080) { routing { static { files("static-data") } } }.start(wait = true) } ``` Done? Not quite. Opening [http://localhost:8080](http://localhost:8080) will only get you a 404, even if you have an `index.html` file in the `static-data` folder. You have to let Ktor know you want to opt-in serving a default file. Let's try again (omitting anything outside the `static` block): ```kotlin static { files("static-data") default("static-data/index.html") } ``` Surely now this works? Well, kind of. The root folder will have a default `index.html`, but sadly if you have any sub-folders with `index.html` files inside, they won't be served by default - that `default()` function only works for the top-level folder. Let's work around that. Let's store our `static-data` directory into a `File` variable, replace the `files` function call by our own extension. Note that this extension will handle the fallback to `index.html` in all cases, so we can also remove the call to `default()`: ```kotlin val staticData = File("/path/to/static-data") static { filesWithDefaultIndex(staticData) } fun Route.filesWithDefaultIndex(dir: File) { } ``` In there, we'll need a `get` handler: ```kotlin fun Route.filesWithDefaultIndex(dir: File) { get("{static_path...}") {} } ``` This will catch anything inside the static path, and let us access the requested path through `call.parameters.getAll("static_path")`. This gets us a list of URL segments that we need to join with `File.separator`: ```kotlin val relativePath = call.parameters .getAll("static_path") ?.joinToString(File.separator) ?: return@get ``` Now, we have three options: - The requested path points to an existing file. Great! We can just return it. - The requested path points to a folder. If there's an `index.html` file in there, let's serve it. - In any other case (the requested path doesn't exist, or points to a folder without an `index.html` file), we let Ktor handle that (most likely returning a 404). We can easily access the corresponding local file, and its fallback if it ends up being a directory: ```kotlin val combinedDir = staticRootFolder?.resolve(dir) ?: dir val file = combinedDir.combineSafe(relativePath) val fallbackFile = file.combineSafe("index.html") ``` Now that we have all those defined, here's how we take the decision on what to serve: ```kotlin val localFile = when { file.isFile -> file file.isDirectory && fallbackFile.isFile -> fallbackFile else -> return@get } ``` And finally, we serve the file: ```kotlin call.respond(LocalFileContent(localFile, ContentType.defaultForFile(localFile))) ``` Great! Let's see what this looks like all together: ```kotlin fun Route.filesWithDefaultIndex(dir: File) { val combinedDir = staticRootFolder?.resolve(dir) ?: dir get("{static_path...}") { val relativePath = call.parameters .getAll("static_path") ?.joinToString(File.separator) ?: return@get val file = combinedDir.combineSafe(relativePath) val fallbackFile = file.combineSafe("index.html") val localFile = when { file.isFile -> file file.isDirectory && fallbackFile.isFile -> fallbackFile else -> return@get } call.respond(LocalFileContent(localFile, ContentType.defaultForFile(localFile))) } } ``` Now if you try to open [http://localhost:8080](http://localhost:8080) or [http://localhost:8080/foo](http://localhost:8080/foo), both should work (assuming there's `index.html` files both at the root and in a folder named `foo`). But if you open [http://localhost:8080/foo/](http://localhost:8080/foo/), with a trailing slash, it still doesn't work. That's because Ktor handles the two routes (with and without trailing slash) differently (for good reasons, see [here](https://youtrack.jetbrains.com/issue/KTOR-372) for some context). In our case, that's not what we want. Luckily, Ktor comes with a plug-in to address this. All that's needed is to install it like any other plugin: ```kotlin install(IgnoreTrailingSlash) ``` And now everything should work as expected.