or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

content-handling.mdcore-client.mdengine-configuration.mdindex.mdplugin-system.mdrequest-building.mdresponse-processing.mdserver-sent-events.mdwebsocket-support.md
tile.json

content-handling.mddocs/

Content Handling

Content serialization, form data, file uploads, and multipart support with type-safe handling. The content handling system provides comprehensive support for various content types, including forms, files, JSON, and custom serialization formats.

Capabilities

Form Data Content

Support for URL-encoded and multipart form data with builder DSL.

/**
 * Form URL-encoded content (application/x-www-form-urlencoded)
 */
class FormDataContent(
    val formData: Parameters
) : OutgoingContent.ByteArrayContent() {
    override val contentLength: Long
    override val contentType: ContentType
    override fun bytes(): ByteArray
}

/**
 * Multipart form data content (multipart/form-data)
 */
class MultiPartFormDataContent(
    parts: List<PartData>,
    val boundary: String = generateBoundary(),
    override val contentType: ContentType = ContentType.MultiPart.FormData.withParameter("boundary", boundary)
) : OutgoingContent.WriteChannelContent() {
    override var contentLength: Long?
    
    override suspend fun writeTo(channel: ByteWriteChannel)
}

/**
 * Generate a unique boundary for multipart content
 */
fun generateBoundary(): String

Form Submission Functions

Convenience functions for submitting forms with various content types.

/**
 * Submit form data using URL encoding or query parameters
 */
suspend fun HttpClient.submitForm(
    url: String,
    formParameters: Parameters,
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse

suspend fun HttpClient.submitForm(
    url: Url,
    formParameters: Parameters, 
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse

/**
 * Submit multipart form data with binary content
 */
suspend fun HttpClient.submitFormWithBinaryData(
    url: String,
    formData: List<PartData>,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse

suspend fun HttpClient.submitFormWithBinaryData(
    url: Url,
    formData: List<PartData>,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpResponse

/**
 * Prepare form submission statements
 */
fun HttpClient.prepareForm(
    url: String,
    formParameters: Parameters,
    encodeInQuery: Boolean = false,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpStatement

fun HttpClient.prepareFormWithBinaryData(
    url: String,
    formData: List<PartData>,
    block: HttpRequestBuilder.() -> Unit = {}
): HttpStatement

Form Building DSL

DSL for building form data with type-safe parameter handling.

/**
 * Build multipart form data using DSL
 */
fun formData(block: FormBuilder.() -> Unit): List<PartData>

/**
 * Builder for constructing multipart form data
 */
class FormBuilder {
    /**
     * Append a text part
     */
    fun append(key: String, value: String, headers: Headers = Headers.Empty)
    
    /**
     * Append a number part
     */
    fun append(key: String, value: Number, headers: Headers = Headers.Empty)
    
    /**
     * Append a byte array part
     */
    fun append(key: String, value: ByteArray, headers: Headers = Headers.Empty)
    
    /**
     * Append a channel content part
     */
    fun append(key: String, value: ByteReadChannel, headers: Headers = Headers.Empty)
    
    /**
     * Append a file part (JVM-specific)
     */
    fun append(key: String, file: File, headers: Headers = Headers.Empty)
    
    /**
     * Append content with custom headers and filename
     */
    fun append(
        key: String,
        value: ByteArray,
        headers: Headers = Headers.Empty,
        filename: String? = null
    )
    
    /**
     * Append streaming content
     */
    fun append(
        key: String,
        provider: ChannelProvider,
        headers: Headers = Headers.Empty,
        filename: String? = null
    )
}

/**
 * Channel provider for streaming content
 */
fun interface ChannelProvider {
    fun invoke(): ByteReadChannel
}

File Content Support (JVM)

JVM-specific file content handling for uploads and serving.

/**
 * JVM file content for uploading files
 */
class LocalFileContent(
    val file: File,
    override val contentType: ContentType = ContentType.defaultForFile(file)
) : OutgoingContent.ReadChannelContent() {
    /** File size in bytes */
    override val contentLength: Long = file.length()
    
    /**
     * Read the entire file content
     */
    override fun readFrom(): ByteReadChannel
    
    /**
     * Read a range of the file content
     */
    override fun readFrom(range: LongRange): ByteReadChannel
}

/**
 * Create LocalFileContent from base directory and relative path
 */
fun LocalFileContent(
    baseDir: File,
    relativePath: String,
    contentType: ContentType = ContentType.defaultForFilePath(relativePath)
): LocalFileContent

/**
 * Get default content type for file extension
 */
fun ContentType.Companion.defaultForFile(file: File): ContentType
fun ContentType.Companion.defaultForFilePath(path: String): ContentType

Part Data Types

Types representing different parts of multipart content.

/**
 * Base interface for multipart form data parts
 */
sealed class PartData {
    abstract val name: String
    abstract val headers: Headers
    
    abstract fun dispose()
}

/**
 * Text part data
 */
class PartData.FormItem(
    override val name: String,
    val value: String,
    override val headers: Headers = Headers.Empty
) : PartData()

/**
 * Binary part data
 */
class PartData.BinaryItem(
    override val name: String,
    val provider: ChannelProvider,
    override val headers: Headers = Headers.Empty,
    val filename: String? = null
) : PartData()

/**
 * File part data
 */
class PartData.FileItem(
    override val name: String,
    val originalFileName: String?,
    val provider: ChannelProvider,
    override val headers: Headers = Headers.Empty
) : PartData()

/**
 * Binary channel item
 */
class PartData.BinaryChannelItem(
    override val name: String,
    val provider: ChannelProvider,
    override val headers: Headers = Headers.Empty
) : PartData()

Observable Content

Content wrapper for monitoring upload/download progress.

/**
 * Content wrapper that observes transfer progress
 */
class ObservableContent<T : OutgoingContent>(
    private val delegate: T,
    private val listener: (Long) -> Unit
) : OutgoingContent by delegate {
    
    override suspend fun writeTo(channel: ByteWriteChannel) {
        val observableChannel = channel.observable(listener)
        delegate.writeTo(observableChannel)
    }
}

/**
 * Create observable content with progress callback
 */
fun <T : OutgoingContent> T.observable(
    listener: (bytesWritten: Long) -> Unit
): ObservableContent<T>

Content Types and Serialization

Content type definitions and serialization support.

/**
 * Set content type for request
 */
fun HttpRequestBuilder.contentType(contentType: ContentType)

/**
 * Set request body with automatic content type detection
 */
fun HttpRequestBuilder.setBody(body: Any)

/**
 * Set request body with explicit type information
 */
fun HttpRequestBuilder.setBody(body: Any, typeInfo: TypeInfo)

/**
 * Common content types
 */
object ContentType {
    object Application {
        val Json: ContentType // application/json
        val Xml: ContentType // application/xml
        val FormUrlEncoded: ContentType // application/x-www-form-urlencoded
        val OctetStream: ContentType // application/octet-stream
        val Pdf: ContentType // application/pdf
        val Zip: ContentType // application/zip
        val ProtoBuf: ContentType // application/x-protobuf
    }
    
    object Text {
        val Plain: ContentType // text/plain
        val Html: ContentType // text/html
        val CSS: ContentType // text/css
        val JavaScript: ContentType // text/javascript
        val CSV: ContentType // text/csv
        val EventStream: ContentType // text/event-stream
    }
    
    object Image {
        val PNG: ContentType // image/png
        val JPEG: ContentType // image/jpeg
        val GIF: ContentType // image/gif
        val SVG: ContentType // image/svg+xml
        val WebP: ContentType // image/webp
    }
    
    object MultiPart {
        val FormData: ContentType // multipart/form-data
        val Mixed: ContentType // multipart/mixed
        val Alternative: ContentType // multipart/alternative
        val Related: ContentType // multipart/related
    }
    
    object Video {
        val MP4: ContentType // video/mp4
        val MPEG: ContentType // video/mpeg
        val QuickTime: ContentType // video/quicktime
    }
    
    object Audio {
        val MP3: ContentType // audio/mpeg
        val MP4: ContentType // audio/mp4
        val OGG: ContentType // audio/ogg
        val WAV: ContentType // audio/wav
    }
}

Request Body Types

Different types of outgoing content for HTTP requests.

/**
 * Base class for all outgoing content
 */
abstract class OutgoingContent {
    abstract val contentType: ContentType?
    abstract val contentLength: Long?
    
    /** Text content with string body */
    abstract class TextContent(
        private val text: String,
        override val contentType: ContentType
    ) : OutgoingContent {
        override val contentLength: Long = text.toByteArray().size.toLong()
    }
    
    /** Binary content with byte array body */
    abstract class ByteArrayContent : OutgoingContent {
        abstract fun bytes(): ByteArray
        override val contentLength: Long? get() = bytes().size.toLong()
    }
    
    /** Content that can be read from a channel */
    abstract class ReadChannelContent : OutgoingContent {
        abstract fun readFrom(): ByteReadChannel
        open fun readFrom(range: LongRange): ByteReadChannel = readFrom()
    }
    
    /** Content that writes to a channel */
    abstract class WriteChannelContent(
        override val contentType: ContentType? = null,
        override val contentLength: Long? = null
    ) : OutgoingContent {
        abstract suspend fun writeTo(channel: ByteWriteChannel)
    }
    
    /** No content (for methods like DELETE, GET) */
    object NoContent : OutgoingContent {
        override val contentLength: Long? = 0L
        override val contentType: ContentType? = null
    }
}

Usage Examples:

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.http.*
import io.ktor.utils.io.*
import java.io.File

val client = HttpClient()

// Submit URL-encoded form
val formResponse = client.submitForm(
    url = "https://api.example.com/login",
    formParameters = Parameters.build {
        append("username", "john_doe")
        append("password", "secret123")
        append("remember", "true")
    }
)

// Submit form as query parameters
val queryResponse = client.submitForm(
    url = "https://api.example.com/search",
    formParameters = Parameters.build {
        append("q", "kotlin ktor")
        append("limit", "10")
    },
    encodeInQuery = true
)

// Submit multipart form with binary data
val multipartResponse = client.submitFormWithBinaryData(
    url = "https://api.example.com/upload",
    formData = formData {
        append("title", "My Document")
        append("description", "Important file upload")
        append("file", File("document.pdf").readBytes(), Headers.build {
            append(HttpHeaders.ContentDisposition, "filename=\"document.pdf\"")
        })
        append("metadata", """{"category": "documents"}""")
    }
)

// Manual form data construction
val manualFormData = formData {
    // Text fields
    append("name", "John Doe")
    append("age", 30)
    append("active", true)
    
    // File upload (JVM)
    val imageFile = File("profile.jpg")
    append("avatar", imageFile, Headers.build {
        append(HttpHeaders.ContentType, "image/jpeg")
    })
    
    // Binary data
    val documentBytes = "Document content".toByteArray()
    append("document", documentBytes, Headers.build {
        append(HttpHeaders.ContentDisposition, "filename=\"document.txt\"")
    }, filename = "document.txt")
    
    // Streaming content
    append("stream", {
        // Return a ByteReadChannel
        ByteReadChannel("Streaming data content")
    }, filename = "stream.txt")
}

val manualResponse = client.post("https://api.example.com/complex-upload") {
    setBody(MultiPartFormDataContent(manualFormData))
}

// File upload with progress monitoring
val largeFile = File("large-video.mp4")
val fileContent = LocalFileContent(largeFile).observable { bytesWritten ->
    val progress = (bytesWritten.toDouble() / largeFile.length() * 100).toInt()
    println("Upload progress: $progress%")
}

val uploadResponse = client.post("https://api.example.com/video-upload") {
    setBody(fileContent)
}

// JSON content handling
val jsonResponse = client.post("https://api.example.com/users") {
    contentType(ContentType.Application.Json)
    setBody("""
        {
            "name": "Jane Smith",
            "email": "jane@example.com",
            "profile": {
                "age": 28,
                "location": "New York"
            }
        }
    """.trimIndent())
}

// XML content handling
val xmlResponse = client.post("https://api.example.com/xml-endpoint") {
    contentType(ContentType.Application.Xml)
    setBody("""
        <?xml version="1.0" encoding="UTF-8"?>
        <user>
            <name>Bob Johnson</name>
            <email>bob@example.com</email>
        </user>
    """.trimIndent())
}

// Custom content with headers
val customResponse = client.post("https://api.example.com/custom") {
    headers {
        append("X-Custom-Header", "custom-value")
        append("Authorization", "Bearer token123")
    }
    contentType(ContentType.Application.OctetStream)
    setBody("Custom binary content".toByteArray())
}

// Streaming request content
val streamingResponse = client.post("https://api.example.com/stream") {
    setBody(object : OutgoingContent.WriteChannelContent() {
        override val contentType = ContentType.Text.Plain
        
        override suspend fun writeTo(channel: ByteWriteChannel) {
            repeat(1000) { i ->
                channel.writeStringUtf8("Line $i\n")
                if (i % 100 == 0) {
                    channel.flush()
                    kotlinx.coroutines.delay(10) // Simulate streaming delay
                }
            }
        }
    })
}

// Form data with content length
val contentLengthForm = FormDataContent(Parameters.build {
    append("field1", "value1")
    append("field2", "value2")
})

val contentLengthResponse = client.post("https://api.example.com/sized-form") {
    setBody(contentLengthForm)
    headers {
        append(HttpHeaders.ContentLength, contentLengthForm.contentLength.toString())
    }
}

client.close()

Parameters Handling

Parameter building and manipulation for forms and URLs.

/**
 * Immutable parameters collection
 */
interface Parameters : StringValues {
    companion object {
        val Empty: Parameters
        
        /** Build parameters using DSL */
        fun build(builder: ParametersBuilder.() -> Unit): Parameters
    }
}

/**
 * Mutable parameters builder
 */
class ParametersBuilder : StringValuesBuilder() {
    /** Build immutable Parameters */
    fun build(): Parameters
}

/**
 * Common string values interface
 */
interface StringValues {
    /** Get all value names */
    fun names(): Set<String>
    
    /** Get all values for a name */
    fun getAll(name: String): List<String>?
    
    /** Get first value for a name */
    operator fun get(name: String): String?
    
    /** Check if name exists */
    fun contains(name: String): Boolean
    
    /** Check if name contains specific value */
    fun contains(name: String, value: String): Boolean
    
    /** Iterate over all entries */
    fun forEach(action: (String, List<String>) -> Unit)
}