{"id":16711930,"url":"https://github.com/danneu/kog","last_synced_at":"2025-03-21T20:33:22.350Z","repository":{"id":142407891,"uuid":"74243588","full_name":"danneu/kog","owner":"danneu","description":"🌶 A simple Kotlin web framework inspired by Clojure's Ring.","archived":false,"fork":false,"pushed_at":"2017-08-06T10:38:41.000Z","size":422,"stargazers_count":43,"open_issues_count":12,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-18T04:52:00.202Z","etag":null,"topics":["http-server","jetty","kotlin"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danneu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-11-20T00:19:09.000Z","updated_at":"2023-08-17T20:32:53.000Z","dependencies_parsed_at":"2024-01-21T11:49:22.916Z","dependency_job_id":"140391e1-ed00-4c56-8f9b-8f3a8ce9ca09","html_url":"https://github.com/danneu/kog","commit_stats":null,"previous_names":[],"tags_count":35,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danneu%2Fkog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danneu%2Fkog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danneu%2Fkog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danneu%2Fkog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danneu","download_url":"https://codeload.github.com/danneu/kog/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244866282,"owners_count":20523491,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http-server","jetty","kotlin"],"created_at":"2024-10-12T20:27:36.556Z","updated_at":"2025-03-21T20:33:21.966Z","avatar_url":"https://github.com/danneu.png","language":"Kotlin","funding_links":[],"categories":["开发框架"],"sub_categories":["Web框架"],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"cogwheel.gif\" width=\"640\" height=\"220\"\u003e\n\u003c/p\u003e\n\n# kog [![Jitpack](https://jitpack.io/v/com.danneu/kog.svg)](https://jitpack.io/#com.danneu/kog) [![Kotlin](https://img.shields.io/badge/kotlin-1.1-blue.svg)](https://kotlinlang.org/) [![Heroku](https://img.shields.io/badge/heroku-ready-8b59b6.svg)](#heroku-deploy) [![Build Status](https://travis-ci.org/danneu/kog.svg?branch=master)](https://travis-ci.org/danneu/kog) [![Dependency Status](https://david-dm.org/danneu/kog.svg)](https://david-dm.org/danneu/kog) ![Stability](https://img.shields.io/badge/stability-experimental-orange.svg)\n\nA simple, experimental Kotlin web framework inspired by Clojure's Ring.\n\nA kog application is a function that takes a `Request` and returns a `Response`.\n\nBuilt on top of [Jetty](http://www.eclipse.org/jetty/).\n\n```kotlin\nimport com.danneu.kog.Server\nimport com.danneu.kog.Response\n\nServer({ Response().text(\"hello world\") }).listen(3000)\n```\n\n## Goals\n\n1. Simplicity\n2. Middleware\n3. Functional composition\n\n## Table of Contents\n\n\u003c!-- toc --\u003e\n\n- [Install](#install)\n- [Quick Start](#quick-start)\n  * [Hello World](#hello-world)\n  * [Type-Safe Routing](#type-safe-routing)\n- [Concepts](#concepts)\n  * [Request \u0026 Response](#request--response)\n  * [Handler](#handler)\n  * [Middleware](#middleware)\n    + [**Tip:** Short-Circuiting Lambdas](#tip-short-circuiting-lambdas)\n- [JSON](#json)\n  * [JSON Encoding](#json-encoding)\n  * [JSON Decoding](#json-decoding)\n- [Routing](#routing)\n  * [Router mounting](#router-mounting)\n- [Cookies](#cookies)\n  * [Request Cookies](#request-cookies)\n  * [Response Cookies](#response-cookies)\n- [Included Middleware](#included-middleware)\n  * [Development Logger](#development-logger)\n  * [Static File Serving](#static-file-serving)\n  * [Conditional-Get Caching](#conditional-get-caching)\n    + [ETag](#etag)\n    + [Last-Modified](#last-modified)\n  * [Multipart File Uploads](#multipart-file-uploads)\n  * [Basic Auth](#basic-auth)\n  * [Compression / Gzip](#compression--gzip)\n- [HTML Templating](#html-templating)\n- [WebSockets](#websockets)\n  * [Idle Timeout](#idle-timeout)\n- [Caching](#caching)\n  * [In-Memory Cache](#in-memory-cache)\n- [Environment Variables](#environment-variables)\n- [Heroku Deploy](#heroku-deploy)\n- [Example: Tiny Pastebin Server](#example-tiny-pastebin-server)\n- [Content Negotiation](#content-negotiation)\n  * [Most acceptable language](#most-acceptable-language)\n- [License](#license)\n\n\u003c!-- tocstop --\u003e\n\n## Install\n\n[![Jitpack](https://jitpack.io/v/com.danneu/kog.svg)](https://jitpack.io/#com.danneu/kog)\n\n``` groovy\nrepositories {\n    maven { url \"https://jitpack.io\" }\n}\n\ndependencies {\n    compile \"com.danneu:kog:x.y.z\"\n    // Or always get latest\n    compile \"com.danneu:kog:master-SNAPSHOT\"\n}\n```\n\n## Quick Start\n\n### Hello World\n\n``` kotlin\nimport com.danneu.kog.Response\nimport com.danneu.kog.Request\nimport com.danneu.kog.Handler\nimport com.danneu.kog.Server\n\nfun handler(req: Request): Response {\n  return Response().html(\"\u003ch1\u003eHello world\u003c/h1\u003e\")\n}\n\n// or use the Handler typealias:\n\nval handler: Handler = { req -\u003e\n  Response().html(\"\u003ch1\u003eHello world\u003c/h1\u003e\") \n}\n\nfun main(args: Array\u003cString\u003e) {\n  Server(handler).listen(3000)\n}\n```\n\n### Type-Safe Routing\n\n``` kotlin\nimport com.danneu.json.Encoder as JE\nimport com.danneu.kog.Router\nimport com.danneu.kog.Response\nimport com.danneu.kog.Request\nimport com.danneu.kog.Handler\nimport com.danneu.kog.Server\n\nval router = Router {\n    get(\"/users\", fun(): Handler = { req -\u003e\n        Response().text(\"list users\")\n    })\n    \n    get(\"/users/\u003cid\u003e\", fun(id: Int): Handler = { req -\u003e\n        Response().text(\"show user $id\")\n    })\n    \n    get(\"/users/\u003cid\u003e/edit\", fun(id: Int): Handler = { req -\u003e\n        Response().text(\"edit user $id\")\n    })\n    \n    // Wrap routes in a group to dry up middleware application\n    group(\"/stories/\u003cid\u003e\", listOf(middleware)) {\n        get(\"/comments\", listOf(middleware), fun(id: java.util.UUID): Handler = { \n            Response().text(\"listing comments for story $id\")\n        })\n    }\n    \n    delete(\"/admin/users/\u003cid\u003e\", listOf(ensureAdmin()), fun(id: Int): Handler = { req -\u003e\n        Response().text(\"admin panel, delete user $id\")\n    })\n    \n    get(\"/\u003ca\u003e/\u003cb\u003e/\u003cc\u003e\", fun(a: Int, b: Int, c: Int): Handler = { req -\u003e\n        Response().json(JE.obj(\"sum\" to JE.num(a + b + c)))\n    })\n  }\n}\n\nval handler = middleware1(middleware2(middleware3(router.handler())))\n\nfun main(args: Array\u003cString\u003e) {\n  Server(handler).listen(3000)\n}\n```\n\n## Concepts\n\nA kog application is simply a function that takes a `Request` and\nreturns a `Response`.\n\n### Request \u0026 Response\n\nThe Request and Response have an API that makes it easy to chain\ntransformations together.\n\nExample junk-drawer:\n\n``` kotlin\nimport com.danneu.kog.Status\nimport com.danneu.kog.json.Encoder as JE\nimport java.util.File\n\nResponse()                                      // skeleton 200 response\nResponse(Status.NotFound)                       // 404 response\nResponse.notFound()       \u003c-- Sugar             // 404 response\nResponse().text(\"Hello\")                        // text/plain\nResponse().html(\"\u003ch1\u003eHello\u003c/h1\u003e\")               // text/html\nResponse().json(JE.obj(\"number\" to JE.num(42))) // application/json {\"number\": 42}\nResponse().json(JE.array(JE.num(1), JE.num(2), JE.num(3))) // application/json [1, 2, 3]\nResponse().file(File(\"video.mp4\"))              // video/mp4 (determines response headers from File metadata)\nResponse().stream(File(\"video.mp4\"))            // video/mp4\nResponse().setHeader(Header.AccessControlAllowOrigin, \"*\")\nResponse().type = ContentType(Mime.Html, mapOf(\"charset\", \"utf-8\"))\nResponse().appendHeader(Header.Custom(\"X-Fruit\"), \"orange\")\nResponse().redirect(\"/\")                           // 302 redirect\nResponse().redirect(\"/\", permanent = true)         // 301 redirect\nResponse().redirectBack(request, \"/\")              // 302 redirect \n```\n\n``` kotlin\nimport com.danneu.kog.json.Decoder as JD\nimport com.danneu.kog.Header\n\n// GET http://example.com/users?sort=created,  json body is {\"foo\": \"bar\"}\nvar handler: Handler = { request -\u003e\n  request.type                     // ContentType(mime=Mime.Html, params=mapOf(\"charset\" to \"utf-8\"))\n  request.href                     // http://example.com/users?sort=created\n  request.path                     // \"/users\"\n  request.method                   // Method.get\n  request.params                   // Map\u003cString, Any\u003e\n  request.json(decoder)            // com.danneu.result.Result\u003cT, Exception\u003e\n  request.utf8()                   // \"{\\\"foo\\\": \\\"bar\\\"}\"\n  request.headers                  // [(Header.Host, \"example.com\"), ...]\n  request.getHeader(Header.Host)   // \"example.com\"?\n  request.getHeader(Header.Custom(\"xxx\"))                 // null\n  request.setHeader(Header.UserAgent, \"MyCrawler/0.0.1\")  // Request\n}\n```\n\n### Handler\n\n``` kotlin\ntypealias Handler = (Request) -\u003e Response\n```\n\nYour application is a function that takes a Request and returns a Response.\n\n``` kotlin\nval handler: Handler = { request in \n  Response().text(\"Hello world\")\n}\n\nfun main(args: Array\u003cString\u003e) {\n  Server(handler).listen(3000)\n}\n```\n\n### Middleware\n\n``` kotlin\ntypealias Middleware = (Handler) -\u003e Handler\n```\n\nMiddleware functions let you run logic when the request is going downstream\nand/or when the response is coming upstream.\n\n``` kotlin\nval logger: Middleware = { handler -\u003e { request -\u003e\n  println(\"Request coming in\")\n  val response = handler(request)\n  println(\"Response going out\")\n  response\n}}\n\nval handler: Handler = { Response().text(\"Hello world\") }\n\nfun main(args: Array\u003cString\u003e) {\n  Server(logger(handler)).listen()\n}\n```\n\nSince middleware are just functions, it's trivial to compose them:\n\n``` kotlin\nimport com.danneu.kog.middleware.compose\n\n// `logger` will touch the request first and the response last\nval middleware = compose(logger, cookieParser, loadCurrentUser)\nServer(middleware(handler)).listen(3000)\n```\n\n#### **Tip:** Short-Circuiting Lambdas\n\nYou often want to bail early when writing middleware and handlers,\nlike short-circuiting your handler with a `400 Bad Request` when the\nclient gives you invalid data.\n\nThe compiler will complain if you `return` inside a lambda expression,\nbut you can fix this by using a `label@`:\n\n``` kotlin\nval middleware: Middleware = { handler -\u003e handler@ { req -\u003e \n    val data = req.query.get(\"data\") ?: return@handler Response.badRequest()\n    Response().text(\"You sent: $data\")\n}}\n```\n\n## JSON\n\nkog wraps the small, fast, and simple [ralfstx/minimal-json][minimal-json] library\nwith combinators for working with JSON.\n\n**Note:** json combinators and the result monad have been extracted from kog:\n\n- [danneu/kotlin-json-combinator][json]\n- [danneu/kotlin-result][result]\n\n[json]: https://github.com/danneu/kotlin-json-combinator\n[result]: https://github.com/danneu/kotlin-result\n[minimal-json]: https://github.com/ralfstx/minimal-json#performance\n\n### JSON Encoding\n\nkog's built-in JSON encoder has these methods: `.obj()`, `.array()`, `.num()`, `.str()`, `.null()`, `.bool()`.\n\nThey all return `com.danneu.json.JsonValue` objects that you pass to `Response#json`.\n\n``` kotlin\nimport com.danneu.json.Encoder as JE\n\nval handler: Handler = { req -\u003e\n  Response().json(JE.obj(\"hello\" to JE.str(\"world\")))\n}\n```\n\n``` kotlin\nimport com.danneu.json.Encoder as JE\n\nval handler: Handler = { req -\u003e\n  Response().json(JE.array(JE.str(\"a\"), JE.str(\"b\"), JE.str(\"c\")))\n  // Or\n  Response().json(JE.array(listOf(JE.str(\"a\"), JE.str(\"b\"), JE.str(\"c\"))))\n}\n```\n\n``` kotlin\nimport com.danneu.json.Encoder as JE\n\nval handler: Handler = { req -\u003e\n  Response().json(JE.obj(\n    \"ok\" to JE.bool(true),\n    \"user\" to JE.obj(\n      \"id\" to JE.num(user.id),\n      \"username\" to JE.str(user.uname),\n      \"luckyNumbers\" to JE.array(JE.num(3), JE.num(9), JE.num(27))\n    )\n  ))\n}\n```\n\nIt might seem redundant/tedious to call `JE.str(\"foo\")` and `JE.num(42)`, but it's type-safe so that\nyou can only pass things into the encoder that's json-serializable. I'm not sure if kotlin supports\nanything simpler at the moment.\n\n### JSON Decoding\n\nkog comes with a declarative JSON parser combinator inspired by Elm's.\n\n`Decoder\u003cT\u003e` is a decoder that will return `Result\u003cT, Exception\u003e` when\ninvoked on a JSON string.\n\n``` kotlin\nimport com.danneu.json.Decoder as JD\nimport com.danneu.json.Encoder as JE\n\n// example request payload: [1, 2, 3]\nval handler = { request -\u003e\n  request.json(JD.array(JD.int)).fold({ nums -\u003e\n    // success\n    Response().json(JE.obj(\"sum\" to JE.num(nums.sum())))\n  }, { parseException -\u003e \n    // failure\n    Response.badRequest()\n  })\n}\n```\n\nWe can use `Result#getOrElse()` to rewrite the previous example so that\ninvalid user-input will defaults to an empty list of numbers.\n\n``` kotlin\nimport com.danneu.json.Decoder as JD\nimport com.danneu.json.Encoder as JE\n\n// example request payload: [1, 2, 3]\nval handler = { req -\u003e\n  val sum = req.json(JD.array(JD.int)).getOrElse(emptyList()).sum()\n  Response().json(JE.obj(\"sum\" to JE.num(sum)))\n}\n```\n\nThis authentication handler parses the username/password combo from\nthe request's JSON body:\n\n``` kotlin\nimport com.danneu.json.Decoder as JD\nimport com.danneu.json.Encoder as JE\n\n// example request payload: {\"user\": {\"uname\": \"chuck\"}, \"password\": \"secret\"}\nval handler = { request -\u003e\n  val decoder = JD.pairOf(\n    JD.get(listOf(\"user\", \"uname\"), JD.string),\n    JD.get(\"password\", JD.string)\n  )\n  val (uname, password) = request.json(decoder)\n  // ... authenticate user ...\n  Response().json(JE.obj(\"success\" to JE.obj(\"uname\" to JE.str(uname))))\n}\n```\n\nCheck out [danneu/kotlin-json-combinator][json] and [danneu/kotlin-result][result]\nfor more examples.\n\n## Routing\n\nkog's router is type-safe because routes only match if the URL params can be parsed into\nthe arguments that your function expects.\n\nAvailable coercions:\n\n- `kotlin.Int`\n- `kotlin.Long`\n- `kotlin.Float`\n- `kotlin.Double`\n- `java.util.UUID`\n\nFor example:\n\n```kotlin\nRouter {\n    // GET /uuid/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa -\u003e 200 Ok\n    // GET /uuid/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA -\u003e 200 Ok\n    // GET /uuid/42                                   -\u003e 404 Not Found\n    // GET /uuid/foo                                  -\u003e 404 Not Found\n    get(\"/uuid/\u003cx\u003e\", fun(uuid: java.util.UUID): Handler = { req -\u003e\n        Response().text(\"you provided a uuid of version ${uuid.version} with a timestamp of ${uuid.timestamp}\")\n    })\n}\n```\n\nHere's a more meandering example:\n\n``` kotlin\nimport com.danneu.kog.json.Encoder as JE\nimport com.danneu.kog.Router\nimport com.danneu.kog.Response\nimport com.danneu.kog.Request\nimport com.danneu.kog.Handler\nimport com.danneu.kog.Server\n\nval router = Router(middleware1(), middleware2()) {\n    get(\"/\", fun(): Handler = { req -\u003e\n        Response().text(\"homepage\")\n    })\n    \n    get(\"/users/\u003cid\u003e\", fun(id: Int): Handler = { req -\u003e\n        Response().text(\"show user $id\")\n    })\n    \n    get(\"/users/\u003cid\u003e/edit\", fun(id: Int): Handler = { req -\u003e\n        Response().text(\"edit user $id\")\n    })\n    \n    // Wrap routes in a group to dry up middleware application\n    group(\"/stories/\u003cid\u003e\", listOf(middleware)) {\n        get(\"/comments\", listOf(middleware), fun(id: java.util.UUID): Handler = { \n            Response().text(\"listing comments for story $id\")\n        })\n    }\n    \n    delete(\"/admin/users/\u003cid\u003e\", listOf(ensureAdmin()), fun(id: Int): Handler = { req -\u003e\n        Response().text(\"admin panel, delete user $id\")\n    })\n    \n    get(\"/\u003ca\u003e/\u003cb\u003e/\u003cc\u003e\", fun(a: Int, b: Int, c: Int): Handler = { req -\u003e\n        Response().json(JE.obj(\"sum\" to JE.num(a + b + c)))\n    })\n    \n    get(\"/hello/world\", fun(a: Int, b: String): Handler = {\n        Response().text(\"this route can never match the function (Int, Int) -\u003e ...\")\n    })\n    \n    get(\"/hello/world\", fun(): Handler = {\n        Response().text(\"this route *will* match\")\n    })\n  }\n}\n\nfun main(args: Array\u003cString\u003e) {\n  Server(handler).listen(3000)\n}\n```\n\n### Router mounting\n\n`Router#mount(subrouter)` will merge a child router into the current router.\n\nUseful for breaking your application into individual routers that you then mount into a top-level router.\n\n```kotlin\nval subrouter = Router {\n    get(\"/foo\", fun(): Handler = { Response() })\n}\n\nval router = Router {\n    mount(subrouter)\n}\n```\n\n```text\ncurl http://localhost:3000/foo      # 200 Ok\n```\n\nOr mount routers at a prefix:\n\n```kotlin\nval subrouter = Router {\n    get(\"/foo\", fun(): Handler = { Response() })\n}\n\nval router = Router {\n    mount(\"/subrouter\", subrouter)\n}\n```\n\n```text\ncurl http://localhost:3000/foo              # 404 Not Found\ncurl http://localhost:3000/subrouter/foo    # 200 Ok\n```\n\nOr mount routers in a group:\n\n```kotlin\nval subrouter = Router {\n    get(\"/foo\", fun(): Handler = { Response() })\n}\n\nval router = Router {\n    group(\"/group\") {\n        mount(\"/subrouter\", subrouter)\n    }\n}\n```\n\n**Note:** The mount prefix must be static. It does not support dynamic patterns like \"/users/\u003cid\u003e\".\n\n## Cookies\n\n### Request Cookies\n\n`Request#cookies` is a `MutableMap\u003cString, String\u003e` which maps cookie names\nto cookie values received in the request.\n\n### Response Cookies\n\n`Response#cookies` is a `MutableMap\u003cString, Cookie\u003e` which maps cookie names\nto cookie objects that will get sent to the client.\n\nHere's a handler that increments a counter cookie on every request that\nwill expire in three days:\n\n``` kotlin\nimport com.danneu.kog.Response\nimport com.danneu.kog.Handler\nimport com.danneu.kog.Server\nimport com.danneu.kog.cookies.Cookie\nimport java.time.OffsetDateTime\n\nfun Request.parseCounter(): Int = try {\n    cookies.getOrDefault(\"counter\", \"0\").toInt()\n} catch(e: NumberFormatException) {\n    0\n}\n\nfun Response.setCounter(count: Int): Response = apply {\n    cookies[\"counter\"] = Cookie(count.toString(), duration = Cookie.Ttl.Expires(OffsetDateTime.now().plusDays(3)))\n}\n\nval handler: Handler = { request -\u003e\n    val count = request.parseCounter() + 1\n    Response().text(\"count: $count\").setCounter(count)\n}\n\nfun main(args: Array\u003cString\u003e) {\n  Server(handler).listen(9000)\n}\n```\n\nDemo:\n\n```\n$ http --session=kog-example --body localhost:9000\ncount: 1\n$ http --session=kog-example --body localhost:9000\ncount: 2\n$ http --session=kog-example --body localhost:9000\ncount: 3\n```\n\n## Included Middleware\n\nThe `com.danneu.kog.batteries` package includes some useful middleware.\n\n### Development Logger\n\nThe logger middleware prints basic info about the request and response \nto stdout.\n\n``` kotlin\nimport com.danneu.kog.batteries.logger\n\nServer(logger(handler)).listen()\n```\n\n![logger screenshot](https://dl.dropboxusercontent.com/spa/quq37nq1583x0lf/_5c9x02w.png)\n\n### Static File Serving\n\nThe serveStatic middleware checks the `request.path` against a directory\nthat you want to serve static files from.\n\n``` kotlin\nimport com.danneu.kog.batteries.serveStatic\n\nval middleware = serveStatic(\"public\", maxAge = Duration.ofDays(365))\nval handler = { Response().text(\":)\") }\n\nServer(middleware(handler)).listen()\n```\n\nIf we have a `public` folder in our project root with a file \n`message.txt`, then the responses will look like this:\n\n    $ http localhost:3000/foo\n    HTTP/1.1 404 Not Found\n\n    $ http localhost:3000/message.txt\n    HTTP/1.1 200 OK\n    Content-Length: 38\n    Content-Type: text/plain\n\n    This is a message from the file system\n\n    $ http localhost:3000/../passwords.txt\n    HTTP/1.1 400 Bad Request\n\n### Conditional-Get Caching \n\nThis middleware adds `Last-Modified` or `ETag` headers to each downstream\nresponse which the browser will echo back on subsequent requests.\n\nIf the response's `Last-Modified`/`ETag` matches the request,\nthen this middleware instead responds with `304 Not Modified` which tells the\nbrowser to use its cache.\n\n#### ETag\n\n`notModified(etag = true)` will generate an ETag header for each\ndownstream response.\n\n``` kotlin\nval router = Router(notModified(etag = true)) {\n    get(\"/\", fun(): Handler = { \n        Response().text(\"Hello, world!) \n    })\n}\n```\n\nFirst request gives us an ETag.\n\n``` bash\n$ http localhost:9000\nHTTP/1.1 200 OK\nContent-Length: 13\nContent-Type: text/plain\nETag: \"d-bNNVbesNpUvKBgtMOUeYOQ\"\n\nHello, world!\n```\n\nWhen we echo back the ETag, the server lets us know that\nthe response hasn't changed:\n\n``` bash\n$ http localhost:9000 If-None-Match:'\"d-bNNVbesNpUvKBgtMOUeYOQ\"'\nHTTP/1.1 304 Not Modified\n```\n\n#### Last-Modified\n\n`notModified(etag = false)` will only add a `Last-Modified` header\nto downstream responses if `response.body is ResponseBody.File` since\nkog can read the mtime from the File's metadata.\n\nIf the response body is not a `ResponseBody.File` type, then no header\nwill be added.\n\nThis is only useful for serving static assets from the filesystem since\nETags are unnecessary to generate when you have a file's modification time.\n\n``` kotlin\nval router = Router {\n    // TODO: kog doesn't yet support mounting middleware on a prefix\n    use(\"/assets\", notModified(etag = false), serveStatic(\"public\", maxAge = Duration.ofHours(4)))\n    get(\"/\") { Response().text(\"homepage\")\n}\n```\n\n### Multipart File Uploads \n\nTo handle file uploads, use the `com.danneu.kog.batteries.multipart` middleware.\n\nThis middleware parses file uploads out of `\"multipart/form-data\"` requests\nand populates `request.uploads : MutableMap\u003cString, SavedUpload\u003e` for your\nhandler to access which is a mapping of field names to `File` representations.\n\n``` kotlin\npackage com.danneu.kog.batteries.multipart\n\nclass SavedUpload(val file: java.io.File, val filename: String, val contentType: String, val length: Long)\n```\n\nIn this early implementation, by the time your handler is executed, the\nfile uploads have already been piped into temporary files in the file-system\nwhich will get automatically deleted.\n\n``` kotlin\nimport com.danneu.kog.Router\nimport com.danneu.kog.batteries.multipart\nimport com.danneu.kog.batteries.multipart.Whitelist\n\nval router = Router {\n    get(\"/\", fun(): Handler = {\n        Response().html(\"\"\"\n            \u003c!doctype html\u003e\n            \u003cform method=\"POST\" action=\"/upload\" enctype=\"multipart/form-data\"\u003e\n                File1: \u003cinput type=\"file\" name=\"file1\"\u003e\n                File2 (Ignored): \u003cinput type=\"file\" name=\"file2\"\u003e\n                \u003cbutton type=\"submit\"\u003eUpload\u003c/button\u003e\n            \u003c/form\u003e\n        \"\"\")\n    })\n    post(\"/upload\", multipart(Whitelist.only(setOf(\"file1\"))), fun(): Handler = { req -\u003e\n        val upload = req.uploads[\"file1\"]\n        Response().text(\"You uploaded ${upload?.length ?: \"--\"} bytes\")\n    })\n}\n\nfun main(args: Array\u003cString\u003e) {\n    Server(router.handler()).listen(3000)\n}\n```\n\nPass a whitelist into `multipart()` to only process field names that\nyou expect.\n\n\n``` kotlin\nimport com.danneu.kog.batteries.multipart\nimport com.danneu.kog.batteries.multipart.Whitelist\n\nmultipart(whitelist = Whitelist.all)\nmultipart(whitelist = Whitelist.only(setOf(\"field1\", \"field2\")))\n```\n\n### Basic Auth\n\nJust pass a `(name, password) -\u003e Boolean` predicate to the\n`basicAuth()` middleware. \n\nYour handler won't get called unless the user satisfies it.\n\n``` kotlin\nimport com.danneu.kog.batteries.basicAuth\n\nfun String.sha256(): ByteArray {\n    return java.security.MessageDigest.getInstance(\"SHA-256\").digest(this.toByteArray())\n}\n\nval secretHash = \"a man a plan a canal panama\".sha256()\n\nfun isAuthenticated(name: String, pass: String): Boolean {\n    return java.util.Arrays.equals(pass.sha256(), secretHash)\n}\n\nval router = Router {\n    get(\"/\", basicAuth(::isAuthenticated)) {\n        Response().text(\"You are authenticated!\")\n    }\n}\n```\n\n### Compression / Gzip \n\nThe `compress` middleware reads and manages the appropriate headers to determine if it should\nsend a gzip-encoded response to the client.\n\nOptions:\n\n- `compress(threshold: ByteLength)` \n   (Default = 1024 bytes)\n   Only compress the response if it is at least this large.\n- `compress(predicate = (String?) -\u003e Boolean)` \n   (Default = Looks up mime in \u003chttps://github.com/jshttp/mime-db\u003e file)\n   Only compress the response if its Content-Type header passes `predicate(type)`.\n   \nSome examples:\n\n``` kotlin\nimport com.danneu.kog.batteries.compress\nimport com.danneu.kog.ByteLength\n\nval router = Router() {\n    // These responses will be compressed if they are JSON of any size\n    group(compress(threshold = ByteLength.zero, predicate = { it == \"application/json\" })) {\n        get(\"/a\", fun(): Handler = { Response().text(\"foo\") })          // \u003c-- Not compressed (not json)\n        get(\"/b\", fun(): Handler = { Response().html(\"\u003ch1\u003ebar\u003c/h1\u003e\") }) // \u003c-- Not compressed (not json)\n        get(\"/c\", fun(): Handler = { Response().jsonArray(1, 2, 3) })   // \u003c-- Compressed\n    }\n    \n    // These responses will be compressed if they are at least 1024 bytes\n    group(compress(threshold = ByteLength.ofBytes(1024))) {\n        get(\"/d\", fun(): Handler = { Response().text(\"qux\") })          // \u003c-- Not compressed (too small)\n    }\n}\n```\n\n## HTML Templating\n\nTemplating libraries generally generate an HTML string. Just pass it to `Response().html(html)`.\n\nFor example, [tipsy/j2html](https://github.com/tipsy/j2html) is a simple templating library\nfor generating HTML from your handlers.\n\n    compile \"com.j2html:j2html:1.0.0\"\n\nHere's an example server with a \"/\" route that renders a file-upload form that posts to a \"/upload\" route.\n\n``` kotlin\nimport j2html.TagCreator.*\nimport j2html.tags.ContainerTag\nimport com.danneu.kog.Router\nimport com.danneu.kog.Response\nimport com.danneu.kog.Server\nimport com.danneu.kog.batteries.multipart\nimport com.danneu.kog.batteries.multipart.Whitelist\n\nfun layout(vararg tags: ContainerTag): String = document().render() + html().with(\n  body().with(*tags)\n).render()\n\nval router: Router = Router {\n    get(\"/\", fun(): Handler = {\n        Response().html(layout(\n          form().attr(\"enctype\", \"multipart/form-data\").withMethod(\"POST\").withAction(\"/upload\").with(\n            input().withType(\"file\").withName(\"myFile\"),\n            button().withType(\"submit\").withText(\"Upload File\")\n          )\n        ))\n    }) \n    post(\"/upload\", multipart(Whitelist.only(setOf(\"myFile\"))), fun(): Handler = {\n        Response().text(\"Uploaded ${req.uploads[\"myFile\"]?.length ?: \"--\"} bytes\")\n    }) \n}\n\nfun main(args: Array\u003cString\u003e) {\n    Server(router.handler()).listen(9000)\n}\n```\n\n## WebSockets\n\nCheck out [examples/websockets.kt][examples-websockets] for a websocket example that demonstrates \na websocket handler that echos back every message, and a websocket handler bound to a dynamic `/ws/\u003cnumber\u003e` route.\n\nTake note of a few limitations explained in the comments that I'm working on fixing.\n\n### Idle Timeout\n\nBy default, Jetty (and thus kog) timeout connections that have idled for 30 seconds.\n\nYou can change this when initializing a kog `Server`:\n\n```kotlin\nimport com.danneu.kog.Server\nimport java.time.Duration\n\nfun main(args: Array\u003cString\u003e) {\n    Server(handler, idleTimeout = Duration.ofMinutes(5)).listen(3000)\n}\n```\n\nHowever, instead of changing kog's `idleTimeout`, you probably want to have your websocket clients ping the server\nto keep the connections alive.\n\nOften reverse proxies like nginx, Heroku's routing layer, and Cloudflare have their own idle timeout. \n\nFor example, here are Heroku's docs on the matter: \u003chttps://devcenter.heroku.com/articles/websockets#timeouts\u003e\n\nI believe this is also why websocket libraries like \u003chttps://socket.io/\u003e implement their own ping/pong.\n\nFinally, it seems that Jetty's maximum idle timeout is 5 minutes, so passing in durations longer than 5 minutes\nseems to just max out at 5 minutes. If someone can correct me here, feel free to create an issue.\n\n[examples-websockets]: https://github.com/danneu/kog/blob/master/src/main/kotlin/com/danneu/kog/examples/websockets.kt\n\n## Caching\n\n### In-Memory Cache\n\nI've been impressed with Ben Manes' [ben-manes/caffeine][caffeine] library.\n\nEasy to pick up and use in any project.\n\nThere's also Guava's [Cache](https://github.com/google/guava/wiki/CachesExplained).\n\n[caffeine]: https://github.com/ben-manes/caffeine\n\n## Environment Variables\n\nKog's `Env` object provides a central way to access any customizations passed into an application.\n\nFirst it reads from an optional `.env` file, then it reads from system properties, and finally it reads\nfrom system environment variables (highest precedence). Any conflicts will be overwritten in that order.\n\nFor instance, if we had `PORT=3000` in an `.env` file and then launched our application with:\n\n    PORT=9999 java -jar app.java\n\nThen this is what we'd see in our code:\n\n``` kotlin\nimport com.danneu.kog.Env\n\nEnv.int(\"PORT\") == 9999\n```\n\nFor example, when deploying an application to Heroku, you want to bind to the port that Heroku gives you\nvia the `\"PORT\"` env variable. But you may want to default to port 3000 in development when there is no port\nconfigured:\n\n``` kotlin\nimport com.danneu.kog.Server\nimport com.danneu.kog.Env\n\nfun main(args: Array\u003cString\u003e) {\n    Server(router.handler()).listen(Env.int(\"PORT\") ?: 3000)\n}\n```\n\n`Env` provides some conveniences:\n\n- `Env.string(key)`\n- `Env.int(key)`\n- `Env.float(key)`\n- `Env.bool(key)`: True if the value is `\"true\"` or `\"1\"`, e.g. `VALUE=true java -jar app.java`\n\nIf the parse fails, `null` is returned.\n\nYou can get a new, overridden env container with `.fork()`:\n\n```kotlin\nEnv.int(\"PORT\")                               //=\u003e 3000\nEnv.fork(mapOf(\"PORT\" to \"8888\")).int(\"PORT\") //=\u003e 8888\nEnv.int(\"PORT\")                               //=\u003e 3000\n```\n\n## Heroku Deploy\n\nThis example application will be called \"com.danneu.kogtest\".\n\nI'm not sure what the minimal boilerplate is, but the following is what worked for me.\n\nIn `./build.gradle`:\n\n``` groovy\nbuildscript {\n    ext.kotlin_version = \"1.1-M03\"\n    ext.shadow_version = \"1.2.3\"\n\n    repositories {\n        jcenter()\n        maven { url  \"http://dl.bintray.com/kotlin/kotlin-eap-1.1\" }\n    }\n\n    dependencies {\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath \"com.github.jengelman.gradle.plugins:shadow:$shadow_version\"\n    }\n}\n\napply plugin: 'kotlin'\napply plugin: 'com.github.johnrengelman.shadow'\napply plugin: 'application'\n\nmainClassName = 'com.danneu.kogtest.MainKt' // \u003c--------------- CHANGE ME\n\nrepositories {\n    jcenter()\n    maven { url  \"http://dl.bintray.com/kotlin/kotlin-eap-1.1\" }\n    maven { url 'https://jitpack.io' }\n}\n\ndependencies {\n    compile \"org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version\"\n    compile 'com.danneu:kog:master-SNAPSHOT'\n}\n\ntask stage(dependsOn: ['shadowJar', 'clean'])\n```\n\nIn `./src/main/kotlin/com/danneu/kogtest/main.kt`:\n\n``` kotlin\npackage com.danneu.kogtest\n\nimport com.danneu.kog.Env\nimport com.danneu.kog.Handler\nimport com.danneu.kog.Response\nimport com.danneu.kog.Server\n\nfun main(args: Array\u003cString\u003e) {\n    val handler: Handler = { Response().text(\"Hello, world!\") }\n    Server(handler).listen(ENV.int(\"PORT\") ?: 3000)\n}\n```\n\n**Reminder:** Bind to the PORT env variable that Heroku will set.\n\nIn `./Procfile`:\n\n```\nweb: java -jar build/libs/kogtest-all.jar\n```\n\nCreate and push to Heroku app:\n\n```\nheroku apps:create my-app\ncommit -am 'Initial commit'\ngit push heroku master\n```\n\n## Example: Tiny Pastebin Server\n\nI got this idea from: \u003chttps://rocket.rs/guide/pastebin/\u003e.\n\nThis simple server will have two endpoints:\n\n- **Upload file**: `curl --data-binary @example.txt http://localhost:3000`.\n  - Uploads binary stream to a \"pastes\" directory on the server.\n  - Server responds with JSON `{ \"url\": \"http://localhost:3000/\u003cuuid\u003e\" }`.\n- **Fetch file**: `curl http://localhost:3000/\u003cuuid\u003e`.\n  - Server responds with file or 404.\n\n``` kotlin\nimport com.danneu.kog.Router\nimport com.danneu.kog.Response\nimport com.danneu.kog.Handler\nimport com.danneu.kog.util.CopyLimitExceeded\nimport com.danneu.kog.util.limitedCopyTo\nimport java.io.File\nimport java.util.UUID\n\nval uploadLimit = ByteLength.ofMegabytes(10)\n\nval router = Router {\n    // Upload file\n    post(\"/\", fun(): Handler = handler@ { req -\u003e\n        // Generate random ID for user's upload\n        val id = UUID.randomUUID()\n        \n        // Ensure \"pastes\" directory is created\n        val destFile = File(File(\"pastes\").apply { mkdir() }, id.toString())\n        \n        // Move user's upload into \"pastes\", bailing if their upload size is too large.\n        try {\n            req.body.limitedCopyTo(uploadLimit, destFile.outputStream())\n        } catch(e: CopyLimitExceeded) {\n            destFile.delete()\n            return@handler Response.badRequest().text(\"Cannot upload more than ${uploadLimit.byteLength} bytes\")\n        }\n        \n        // If stream was empty, delete the file and scold user\n        if (destFile.length() == 0L) {\n            destFile.delete()\n            return@handler Response.badRequest().text(\"Paste file required\")\n        }\n        \n        println(\"A client uploaded ${destFile.length()} bytes to ${destFile.absolutePath}\")\n        \n        // Tell user where they can find their uploaded file\n        Response().json(JE.obj(\"url\" to JE.str(\"http://localhost:${req.serverPort}/$id\")))\n    })\n    \n    // Fetch file\n    get(\"/\u003cid\u003e\", fun(id: UUID): Handler = handler@ { req -\u003e\n        val file = File(\"pastes/$id\")\n        if (!file.exists()) return@handler Response.notFound()\n        Response().file(file)\n    })\n}\n\nfun main(args: Array\u003cString\u003e) {\n    Server(router.handler()).listen(3000)\n}\n```\n\n## Content Negotiation\n\n**TODO:** Improve negotiation docs\n\nEach request has a negotiator that parses the `accept-*` headers, returning a list of \nvalues in order of client preference.\n\n- `request.negotiate.mediaTypes` parses the `accept` header.\n- `request.negotiate.languages` parses the `accept-language` header.\n- `request.negotiate.encodings` parses the `accept-encoding` header.\n\nUntil the docs are fleshed out, here's a demo server that will illuminate this:\n\n```kotlin\nfun main(args: Array\u003cString\u003e) {\n    val handler: Handler = { request -\u003e\n        println(request.headers.toString())\n        Response().text(\"\"\"\n        languages:  ${request.negotiate.languages()}\n        encodings:  ${request.negotiate.encodings()}\n        mediaTypes: ${request.negotiate.mediaTypes()}\n        \"\"\".trimIndent())\n    }\n\n    Server(handler).listen(3000)\n}\n```\n\nAn example curl request:\n\n```text\ncurl http://localhost:3000 \\\n  --header 'Accept-Language:de;q=0.7, fr-CH, fr;q=0.9, en;q=0.8, *;q=0.5, de-CH;q=0.2' \\\n  --header 'accept:application/json,TEXT/*' \\\n  --header 'accept-encoding:gzip,DeFLaTE'\n```\n\nCorresponding response:\n\n```text\nlanguages:  [French[CH], French[*], English[*], German[*], *[*], German[CH]]\nencodings:  [Encoding(name='gzip', q=1.0), Encoding(name='deflate', q=1.0)]\nmediaTypes: [MediaType(type='application', subtype='json', q=1.0), MediaType(type='text', subtype='*', q=1.0)]\n```\n\nNotice that values (\"TEXT/*\", \"DeFLaTE\") are always downcased for easy comparison.\n\n### Most acceptable language\n\nGiven a list of languages that you want to support, the negotiator can return a list that filters and sorts your \navailable languages down in order of client preference, the first one being the client's highest preference.\n\n```kotlin\nimport com.danneu.kog.Lang\nimport com.danneu.kog.Locale\n\n// Request Accept-Language: \"en-US, es\"\nrequest.negotiate.acceptableLanguages(listOf(\n    Lang.Spanish(),\n    Lang.English(Locale.UnitedStates)\n)) == listOf(\n    Lang.English(Locale.UnitedStates),\n    Lang.Spanish()\n)\n```\n\nAlso, note that we don't have to provide a locale. If the client asks for `en-US`, then of course\n`Lang.English()` without a locale should be acceptable if we have no more specific match.\n\n```kotlin\n// Request Accept-Language: \"en-US, es\"\nrequest.negotiate.acceptableLanguages(listOf(\n    Lang.Spanish(),\n    Lang.English()\n)) == listOf(\n    Lang.English(),\n    Lang.Spanish()\n)\n```\n\nThe singular form, `.acceptableLanguage()`, is a helper that returns the first result (the most preferred language\nin common with the client).\n\n```kotlin\n// Request Accept-Language: \"en-US, es\"\nrequest.negotiate.acceptableLanguage(listOf(\n    Lang.Spanish(),\n    Lang.English()\n)) == Lang.English()\n```\n\nHere we write an extension function `Request#lang()` that returns the optimal lang between\nour available langs and the client's requested langs.\n\nWe define an internal `OurLangs` enum so that we can exhaust it with `when` expressions in our routes\nor middleware.\n\n```kotlin\nenum class OurLangs {\n    Spanish,\n    English\n}\n\nfun Request.lang(): OurLangs {\n    val availableLangs = listOf(\n        Lang.Spanish(),\n        Lang.English()\n    )\n    \n    return when (this.negotiate.acceptableLanguage(availableLangs)) {\n        Lang.English() -\u003e OurLangs.English\n        // Default to Spanish\n        else -\u003e OurLangs.Spanish\n    }\n}\n\nrouter.get(\"/\", fun(): Handler = { request -\u003e \n    return when (request.lang()) {\n        OurLangs.Spanish() -\u003e\n            Response().text(\"Les servimos en español\")\n        OurLangs.English() -\u003e\n            Response().text(\"We're serving you English\")\n    }\n})\n```\n\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanneu%2Fkog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanneu%2Fkog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanneu%2Fkog/lists"}