Request & response
Every piece of HTTP code in Kona works with two small classes from the org.konaframework.http package:
Request— an immutable snapshot of what the client sent: method, path, headers, query string, and body. It also carries a mutable attributes map that Kona and your middleware use to pass values along (route parameters live there).Response— what you send back: a status code, a string body, and a map of headers. Responses are mutable and chainable, so middleware can adjust them on the way out.
A handler is any function that takes a Request and returns a Response (the Handler interface). Route actions, controller methods, and the end of a middleware chain are all handlers. This page explains both classes in detail; Routing explains how a request reaches your handler, and Kernel & server explains how the bytes on the socket become a Request in the first place.
Where a Request comes from
Section titled “Where a Request comes from”When the embedded server (KonaHttpServer) receives a call, it builds a Request from the raw exchange:
| Field | Source |
|---|---|
method |
The HTTP method as sent, for example GET or POST |
path |
The raw request path, without the query string, still percent-encoded (for example /files/a%20b) |
headers |
One entry per header name; when a client repeats a header, only the first value is kept |
query |
The query string parsed into name → value, percent-decoded; a repeated name keeps the last value, and ?flag with no = becomes flag → "" |
bodyBytes |
Raw request body bytes (binary-safe) |
body |
UTF-8 string view of the body (empty when the server only supplied bytes) |
attributes |
Starts empty; the kernel adds route parameters, middleware can add anything |
The embedded server stores the wire body as bodyBytes. Text helpers (form for URL-encoded/JSON, logging) use a UTF-8 body string when you construct requests in tests. There is no streaming body API yet, so large uploads are held in memory.
You can also build requests yourself, which is how most HTTP tests in Kona are written — you hand the request straight to the kernel without opening a socket:
val get = Request.get("/users/42")val form = Request.post("/login", "email=ada%40example.com&password=secret")val json = Request( method = "POST", path = "/users", headers = mapOf("Content-Type" to "application/json"), body = """{"name":"Ada","age":36}""",)Request get = Request.get("/users/42");Request form = Request.post("/login", "email=ada%40example.com&password=secret");Request json = new Request( "POST", "/users", Map.of("Content-Type", "application/json"), Map.of(), // query "{\"name\":\"Ada\",\"age\":36}" // body);The constructor’s trailing parameters are optional in both languages: Java can call new Request("DELETE", "/users/42") and stop there. Request.post takes the body as a second argument; Kotlin may omit it, Java passes it explicitly (use "" for an empty body).
In Java, the fields are read through getters: getMethod(), getPath(), getHeaders(), getQuery(), getBody(), getAttributes().
Headers
Section titled “Headers”Use header(name) to read a header. The lookup is case-insensitive, so header("content-type") and header("Content-Type") return the same value. A second overload supplies a default when the header is absent.
val agent = request.header("User-Agent") // String? — null when missingval accept = request.header("Accept", "*/*") // falls back to "*/*"String agent = request.header("User-Agent"); // may be nullString accept = request.header("Accept", "*/*");Prefer header(...) over reading the headers map directly. The map keeps the names exactly as the server delivered them, and map lookups are case-sensitive.
Query string and form fields
Section titled “Query string and form fields”Kona gives you three ways to read simple string input, and the difference between them matters when the same name appears in both the query string and the body.
The parsed query string, exactly as described in the table above. request.query["page"] in Kotlin, request.getQuery().get("page") in Java.
form()
Section titled “form()”form() parses the body into a Map<String, String>:
- An empty body gives an empty map.
- If the
Content-Typeheader containsapplication/json(case-insensitive, soapplication/json; charset=utf-8counts), the body is read as a JSON object and flattened to strings: text values stay as-is,nullbecomes"", and numbers, booleans, arrays, and nested objects become their JSON text (36,true,["a","b"]). A JSON body that is not an object (for example a top-level array) gives an empty map. - If the content type is
multipart/form-data, text fields are parsed into the map; file parts are available viafile/hasFile/allFiles(see below). - For URL-encoded forms (
application/x-www-form-urlencodedor a missing content type), the body is parsed asa=1&b=two, with names and values percent-decoded. A repeated field keeps the last value.
form() parses on every call; store the result in a local variable if you read it many times.
input(key, default) and all()
Section titled “input(key, default) and all()”input(key, default)reads fromdata()(query + body + anymergeoverlay) and stringifies the value.all()returns the same bag as strings (null values omitted).
val page = request.input("page", "1")val search = request.input("q") // default is nullval everything: Map<String, String> = request.all()String page = request.input("page", "1");String search = request.input("q"); // one-arg overload; may be nullMap<String, String> everything = request.all();Only, except, presence helpers
Section titled “Only, except, presence helpers”| API | Meaning |
|---|---|
only("a", "b") |
Subset of data() for the listed keys |
except("a") |
data() without those keys |
has("a") |
Key exists (including null) |
filled("a") |
Key exists and is not empty |
missing("a") |
Key is absent |
whenHas / whenFilled / whenMissing |
Run a callback when the condition holds |
Boolean, date, bearer, IP, user agent
Section titled “Boolean, date, bearer, IP, user agent”| API | Meaning |
|---|---|
boolean("on") |
Truthy for 1 / true / on / yes (Java: booleanValue) |
date("when") |
ISO local date / date-time → LocalDate, or null |
bearerToken() |
Token from Authorization: Bearer … |
ip() |
kona.clientIp after TrustProxies, else kona.remoteAddr |
userAgent() |
User-Agent header |
Merge and replace
Section titled “Merge and replace”merge(map) copies the current input bag (or natural input on first call) and overlays new values. replace(map) swaps the entire bag. Both affect data(), input, validate, and related helpers for the rest of the request.
validate and safe
Section titled “validate and safe”validate(rules) runs the Validator and remembers the result. After a successful call, safe() returns that same map. Calling safe() before validate throws IllegalStateException.
Typed JSON with data()
Section titled “Typed JSON with data()”form() and all() always give you strings, which is right for HTML forms but loses information for JSON APIs. data() returns Map<String, Any?> with natural JSON types:
| JSON value | Kotlin / Java type in data() |
|---|---|
"Ada" |
String |
36 |
Int / Integer (larger values become Long or BigInteger) |
1.5 |
Double |
true |
Boolean |
null |
null |
[...] |
List |
{...} |
Map |
When the request is JSON (same Content-Type check as above), data() returns the query string merged with the parsed body object, with body keys winning on conflict. For multipart, data() merges query, text fields, and UploadedFile values under their field names. For other requests, data() returns all(), so all values are strings.
Uploaded files
Section titled “Uploaded files”For multipart/form-data:
| API | Meaning |
|---|---|
file(name) |
The UploadedFile, or null |
hasFile(name) |
True when the file is present and valid (non-empty, no upload error) |
allFiles() |
Map of field name → UploadedFile |
UploadedFile exposes clientOriginalName, clientMimeType, contents, size, extension(), hashName(), and store / storeAs (writes through a FilesystemAdapter).
val payload = request.data()val tags = payload["tags"] as? List<*> ?: emptyList<Any>()val active = payload["active"] == trueMap<String, Object> payload = request.data();Object tags = payload.get("tags"); // a java.util.List for a JSON arrayboolean active = Boolean.TRUE.equals(payload.get("active"));Validation reads input through data(), which is why rules such as boolean, array, and integer behave correctly for JSON bodies.
Attributes
Section titled “Attributes”attributes is a mutable map that lives for exactly one request. It is the standard place for one stage of the pipeline to hand data to a later one: an authentication middleware stores the current user, a handler reads it.
request.setAttribute("user", currentUser) // returns the Request, so calls chainval user = request.getAttribute("user") as User?request.setAttribute("user", currentUser);User user = (User) request.getAttribute("user");The kernel writes route parameters here before your handler runs. For a route /users/{id} and the path /users/42, both getAttribute("id") and getAttribute("route.id") return "42" (always a String). Use the route. form when a plain name could collide with something your middleware stores.
Route parameters (request.route)
Section titled “Route parameters (request.route)”After the router matches a path, the kernel stores each {param} on the request under both param and route.param. Prefer the dedicated helper:
| API | Behavior |
|---|---|
route(key) |
String value or null |
route(key, default) |
String value or default |
val id = request.route("id")!!val slug = request.route("slug", "home")String id = request.route("id");String slug = request.route("slug", "home");Values are always strings from the path. Details and encoding rules: Routing → Path parameters.
Building a Response
Section titled “Building a Response”Factories
Section titled “Factories”Static factories cover the common cases. Body factories set a Content-Type for you; the first three accept an optional status code (default 200).
| Factory | Body | Content-Type / notes |
|---|---|---|
Response.text(body, status) |
The string as given | text/plain; charset=utf-8 |
Response.html(body, status) |
The string as given | text/html; charset=utf-8 |
Response.json(payload, status) |
payload serialized to JSON |
application/json; charset=utf-8 |
Response.noContent() |
Empty, status 204 |
text/plain; charset=utf-8 (no body is sent) |
Response.redirect(to, status) |
Empty, status 302 by default |
Sets Location |
Response.back(request, fallback, status) |
Empty redirect | Previous URL → Referer → fallback |
Response.binary(bytes, contentType, status) |
Raw bytes | Defaults to application/octet-stream |
Response.stream(writer, contentType, status) |
Bytes produced by writing to an OutputStream (buffered in memory for adapters) |
Defaults to application/octet-stream |
| `Response.file(bytes | path, …)` | Inline file bytes |
| `Response.download(bytes | path, filename, …)` | Attachment bytes |
json accepts any value the bundled Jackson serializer understands: maps, lists, strings, numbers, booleans, null (serialized as null), Kotlin data classes, Java records, and ordinary classes with getters.
Binary factories set bodyBytes() so the embedded server writes the payload without UTF-8 re-encoding. Prefer them for PDFs, images, and anything that is not text.
Chain header(name, value), withHeaders(map), cookie(...), withStatus, and withBody / withBodyBytes on any response. Response.stream lets app code write incrementally to an OutputStream; the result is buffered so every server adapter can emit a complete body today.
data class UserView(val id: Int, val name: String)
router.get("/hello") { Response.text("Hello") }router.get("/page") { Response.html("<h1>Welcome</h1>") }router.get("/users/{id}") { Response.json(UserView(42, "Ada")) }router.post("/users") { Response.json(mapOf("id" to 43), 201) }router.delete("/users/{id}") { Response.noContent() }router.post("/save") { Response.redirect("/done").with("status", "Saved") }router.get("/report.pdf") { Response.download(pdfBytes, "report.pdf") }record UserView(int id, String name) {}
router.get("/hello", request -> Response.text("Hello"));router.get("/page", request -> Response.html("<h1>Welcome</h1>"));router.get("/users/{id}", request -> Response.json(new UserView(42, "Ada")));router.post("/users", request -> Response.json(Map.of("id", 43), 201));router.delete("/users/{id}", request -> Response.noContent());router.post("/save", request -> Response.redirect("/done").with("status", "Saved"));router.get("/report.pdf", request -> Response.download(pdfBytes, "report.pdf"));Redirects
Section titled “Redirects”Response.redirect("/path") returns a redirect with a Location header. Chain flash helpers for the next request (applied by StartSession):
with(key, value)— flash a valuewithInput(map)— flash_old_inputfor form redisplay (request.old("field"))withErrors(map)— flash validationerrors
Response.back(request) targets the session’s previous URL (recorded on successful GETs), then the Referer header, then /.
Adjusting a response
Section titled “Adjusting a response”Response has public, mutable status, body, and headers, plus binary bodyBytes(). The fluent helpers each change one part and return the same object:
header(name, value)sets a header, replacing any earlier value stored under the same name.withStatus(code)changes the status.withBody(content)replaces the text body (clears any binary payload).withBodyBytes(bytes)sets a binary payload for the server to write as-is.
return Response.json(mapOf("ok" to true)) .withStatus(202) .header("Location", "/jobs/7") .header("Cache-Control", "no-store")return Response.json(Map.of("ok", true)) .withStatus(202) .header("Location", "/jobs/7") .header("Cache-Control", "no-store");The no-argument constructor (Response() / new Response()) gives status 200, an empty body, and a text/plain content type; it is handy when middleware builds a response from scratch. In Java, getStatus()/setStatus(...), getBody()/setBody(...), and getHeaders() expose the same fields.
Two details about response headers:
- Header names in a
Responseare stored exactly as written. To replace the content type set by a factory, use the same spelling,Content-Type; a different spelling such ascontent-typeadds a second entry instead. - A response holds one value per header name. Calling
header("Set-Cookie", …)twice keeps only the second value.
Cookies
Section titled “Cookies”Read cookies from the Cookie request header:
| API | Behavior |
|---|---|
cookies() |
Map of name → value |
cookie(name) / cookie(name, default) |
Single cookie |
On the response side, chain cookie(Cookie) or cookie(name, value, …) with path, domain, maxAge, secure, httpOnly, and sameSite options. See the Cookie type in org.konaframework.http.
Session, old input, and JSON expectation
Section titled “Session, old input, and JSON expectation”| API | Behavior |
|---|---|
session() |
RequestSession from attribute "session" when StartSession ran |
old(key) / old(key, default) |
Flashed old input for form redisplay |
expectsJson() |
True when Accept prefers JSON without HTML, or Content-Type is JSON — drives validation error shape |
Flash helpers on redirects (with, withInput, withErrors) are applied when the session saves — see Validation and Session.
What the server does with your response
Section titled “What the server does with your response”After the kernel returns, the server writes every header, then the body encoded as UTF-8 with a matching Content-Length. For 204 No Content responses and for HEAD requests, no body is sent at all. See Kernel & server for the full sequence.
API reference (high level)
Section titled “API reference (high level)”Request
Section titled “Request”Construction: Request(...), Request.get(path), Request.post(path, body).
Reading: header, query, form, input, all, data, only/except, has/filled/missing, whenHas/whenFilled/whenMissing, boolean, date, bearerToken, ip, userAgent, route, file/hasFile/allFiles, cookies/cookie, session/old, expectsJson, validate/safe, merge/replace, attributes.
Response
Section titled “Response”Factories: text, html, json, noContent, redirect, back, binary, file, download.
Mutators: header, withHeaders, cookie, withStatus, withBody, withBodyBytes, with, withInput, withErrors.
Pitfalls and FAQ
Section titled “Pitfalls and FAQ”Why is input("id") empty but the path has an id?
Path parameters are attributes (route("id")), not form/query input.
Why do I get 500 on bad JSON?
Malformed JSON throws from the parser; the default handler treats it as an unexpected error. Catch it yourself for a 400.
Can I stream a large file?
Not yet — file / download / binary load bytes in memory.
What is not available today
Section titled “What is not available today”- Streaming request or response bodies
- Multi-value headers on
Request(first wins) orResponse(one value per name) - Automatic
_methodoverride from form bodies - Content negotiation beyond what you implement in handlers
Next steps
Section titled “Next steps”- Map URLs in Routing.
- Validate with Validation.
- Handle failures in Exceptions.