Download Latest Version v0.24.0 source code.tar.gz (668.9 kB)
Email in envelope

Get an email when there's a new version of Kobweb

Home / v0.24.0
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-02-17 9.6 kB
v0.24.0 source code.tar.gz 2026-02-17 668.9 kB
v0.24.0 source code.zip 2026-02-17 1.1 MB
Totals: 3 Items   1.8 MB 1

In this update, we took some time to focus on the backend API, adding support for multipart requests. We also revisited the associated frontend APIs (and deprecated a bunch of them) paving the way for more improvements to follow.

This release also makes markdown handling more robust, and it raises the versions of kotlin to 2.3.10, compose (html) to 1.10.0, and compose (runtime) to 1.10.2 (now sourced from androidx).

:::toml
[versions]
compose-runtime = "1.10.2"
compose-html = "1.10.0"
kobweb = "0.24.0"
kotlin = "2.3.10"

[libraries]
compose-html-core = { module = "org.jetbrains.compose.html:html-core", version.ref = "compose-html" }
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose-runtime" }

[!IMPORTANT] Planning to upgrade? Review instructions in the README.

Changes

Frontend

  • Added missing bindings to support window.getSelection and document.getSelection
  • Added new SVG icons
  • Added holdsIn contracts to Modifier.thenIf/Unless and CssStyleVariant.thenIf/Unless
  • This allows the Kotlin compiler to smart-cast values within the then lambda block.
  • Fixed Modifier.displayBetween which was broken since a prior rename
  • Improved validation for dynamic route segments
  • Optional route segments (e.g. {param?}) are now only allowed in the final position of a route
  • An error is shown if an optional segment is unnecessary (i.e. the non-optional route already exists)
  • Conflicting dynamic route segments now produce an error at compile time
  • Enabled the unused return value checker across the codebase
  • Consider enabling the feature in your own project! For example, this can highlight a common mistake where people forget to put a base block in their CssStyle block.
  • Added "...bytes" versions of frontend HTTP helper methods that return raw bytes (e.g. window.api.bytes(...) changed to window.api.getBytes(...))
  • This more explicit naming convention will allow us to reclaim the existing methods in a future version but change them to return a more expressive return type.

Backend

  • Added support for multipart request bodies.
  • Updated Request.Body and Response.Body APIs as a side effect
  • See Notes for more details.

  • Added a user data property on Request and Response classes

  • This allows passing custom data around when using API interceptors

Markdown

  • Overhauled HTML handling in markdown to produce safer, more correct output
  • HTML attributes in markdown are now parsed and applied using attrs lambda blocks instead of string manipulation
  • Fixed multiple escaping issues (backslashes, dollar signs, newlines, html blocks, and inline html)
  • Safely handle HTML attributes that use tick delimiters containing quotes

Gradle

  • Fixed a null error that could occur in the Gradle task listener
  • Internal build improvements (migrated away from kotlin-dsl plugin, added Gradle assignment plugin)

Misc

  • Fixed crash when running using the Jetbrains Runtime JDK
  • Migrated the Compose runtime dependency to androidx
  • Updated many dependencies to their latest versions (Kotlin 2.3.10, KSP, ktor, Compose, and more)
  • Thanks to fixes in these dependencies, you should now feel free to use the latest versions of Kotlin and Compose, without waiting for a new Kobweb release.

Thanks!

  • @christomah0 contributed new SVG icons to the project.

Notes

Multipart requests

Requests using multipart forms is outside the scope of these release notes, but consider reading the official docs on the topic for more information.

Kobweb does not yet have an idiomatic way to send a multipart request (we will add this into a future release, but you can use the stdlib for this, leveraging FormData, RequestInit, and window.fetch APIs for now.)

The following FE code (imagine in a @Page somewhere) lets the user pick a file from their machine and send it to the server, using multipart forms to send both the file itself and an extra description as metadata:

:::kotlin
val scope = rememberCoroutineScope()
var filePath by remember { mutableStateOf("") }
Input(InputType.File, filePath, onValueChange = { filePath = it }, Modifier.id("file-input"))
Button(onClick = {
   val fileInput = document.getElementById("file-input") as HTMLInputElement
   val file = fileInput.files?.get(0)
      if (file != null) {
         scope.launch {
            val formData = FormData().apply {
               append("file", file, file.name)
               append("description", "Kobweb multipart test")
            }

            val requestInit = RequestInit(method = "POST", body = formData)
            val response = window.fetch("/api/multipart", requestInit).await()
            if (response.ok) {
               response.text().await().let { window.alert(it) }
            } else {
               window.alert("Upload failed: ${response.status}")
            }
         }
      }
   }
}

Then, to handle a multipart request on the backend, write an API endpoint that uses req.body.multipart() to query it. The following endpoint is just a demo and returns diagnostic information about what was sent back as body text:

:::kotlin
@Api
suspend fun multipart(ctx: ApiContext) {
   val mp = ctx.req.body?.multipart() ?: return
   ctx.res.body = Body.text(buildString {
      appendLine("Received multipart request")
      var i = 0
      mp.forEachPart { part ->
         appendLine("\nPart #${++i}")
         appendLine("- Disposition: ${part.contentDisposition!!.disposition}")
         appendLine("- Name: ${part.contentDisposition!!.name}")
         (part.extras as? Multipart.Extras.File)?.let { fileExtras ->
            appendLine("- Original file name: ${fileExtras.originalFileName}")
         }
         val bytes = part.bytes()
         appendLine("- Size: ${bytes.size} bytes")
         val maxSize = 100
         if (bytes.size > maxSize) {
            appendLine("- Content (first $maxSize bytes): ${bytes.take(maxSize).toByteArray().decodeToString().replace("\n", "\\n")}...")
         } else {
                appendLine("- Content: ${bytes.decodeToString().replace("\n", "\\n")}")
         }
      }
   })
}

Our API is inspired by, but (we think) slightly more Kotlin idiomatic than, the ktor version.

Migrating frontend / backend APIs

Changes in this release caused us to reevaluate our APIs used for communicating between the frontend and backend, after adding multipart support challenged simple assumptions that I had mistakenly made a long time ago.

Fortunately, migration should be pretty easy, and it is hoped that most projects won't even notice the changes, or at least, will just need to accept IDE suggestions on deprecated lines of code.

HTTP responses expecting raw bytes

The APIs that send HTTP verb requests and expect raw bytes back have been renamed to be more explicit, e.g.

:::kotlin
// Before
window.api.get("get-endpoint").let { bytes -> ... }
window.api.post("post-endpoint").let { bytes -> ... }

// After
window.api.getBytes("get-endpoint").let { bytes -> ... }
window.api.postBytes("post-endpoint").let { bytes -> ... }

In a near future release of Kobweb, the old get, post, etc. methods will be repurposed to return a different, richer type.

Reading / writing body text

Before, we put read/write extension methods directly on the Request and Response objects, but body content can be more flexible than I originally realized, so I tweaked these APIs and moved them onto the exposed body objects instead.

:::kotlin
// Before
ctx.res.setBodyText(text)
val text = ctx.req.readBodyText()!!

// After
ctx.res.body = Body.text(text)
val text = ctx.req.body!!.text()

We also now support alternate ways of reading body content, i.e. body.stream() and body.bytes()

Querying body text is async now

Previously, Kobweb read the bytes out of the body itself on the backend and converted it into a string, but this was not compatible with some kind of response bodies. So now, we let the user query the body contents at their convenience.

If you run into an error that an API call you wrote cannot call a suspend method, this generally means you may have to update its signature to be explicitly suspend:

:::kotlin
// Before
@Api
fun endpoint(ctx: ApiContext) { ... }

// After
@Api
suspend fun endpoint(ctx: ApiContext) { ... }

Full Changelog: https://github.com/varabyte/kobweb/compare/v0.23.3...v0.24.0

Source: README.md, updated 2026-02-17