or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdcontent-multipart.mdcontent-types.mdcookies.mdheaders-parameters.mdhttp-protocol.mdindex.mdurl-handling.md
tile.json

content-multipart.mddocs/

Content and Multipart Data

Outgoing content abstractions for different data types including text, binary data, streaming content, and comprehensive multipart form data processing for file uploads and form submissions.

OutgoingContent Classes

Base OutgoingContent

sealed class OutgoingContent {
    abstract val contentType: ContentType?
    abstract val contentLength: Long?
    abstract val status: HttpStatusCode?
    abstract val headers: Headers
    
    fun getProperty(key: AttributeKey<T>): T?
    fun setProperty(key: AttributeKey<T>, value: T?)
    fun trailers(): Headers?
}

NoContent

class OutgoingContent.NoContent : OutgoingContent() {
    override val contentLength: Long? = 0L
    override val contentType: ContentType? = null
    override val status: HttpStatusCode? = null
    override val headers: Headers = Headers.Empty
}

Represents empty content with no body.

ByteArrayContent

class OutgoingContent.ByteArrayContent(
    private val bytes: ByteArray
) : OutgoingContent() {
    override val contentLength: Long = bytes.size.toLong()
    override val contentType: ContentType? = null
    override val status: HttpStatusCode? = null
    override val headers: Headers = Headers.Empty
    
    fun bytes(): ByteArray
}

ReadChannelContent

abstract class OutgoingContent.ReadChannelContent : OutgoingContent() {
    abstract fun readFrom(): ByteReadChannel
}

For streaming content that can be read from a channel.

WriteChannelContent

abstract class OutgoingContent.WriteChannelContent : OutgoingContent() {
    abstract suspend fun writeTo(channel: ByteWriteChannel)
}

For content that writes to a channel.

ProtocolUpgrade

abstract class OutgoingContent.ProtocolUpgrade : OutgoingContent() {
    abstract suspend fun upgrade(
        input: ByteReadChannel,
        output: ByteWriteChannel,
        engineContext: CoroutineContext,
        userContext: CoroutineContext
    ): Job
}

For protocol upgrade scenarios like WebSocket handshakes.

ContentWrapper

class OutgoingContent.ContentWrapper(
    val delegate: OutgoingContent,
    override val contentType: ContentType?,
    override val contentLength: Long?,
    override val status: HttpStatusCode?,
    override val headers: Headers
) : OutgoingContent()

Wraps other content with modified properties.

Concrete Content Types

TextContent

class TextContent(
    val text: String,
    override val contentType: ContentType,
    override val status: HttpStatusCode? = null
) : OutgoingContent() {
    override val contentLength: Long = text.toByteArray(contentType.charset() ?: Charsets.UTF_8).size.toLong()
    override val headers: Headers = Headers.Empty
}

ByteArrayContent

class ByteArrayContent(private val bytes: ByteArray) : OutgoingContent() {
    override val contentType: ContentType? = null
    override val contentLength: Long = bytes.size.toLong()
    override val status: HttpStatusCode? = null
    override val headers: Headers = Headers.Empty
    
    fun bytes(): ByteArray = bytes
}

ChannelWriterContent

class ChannelWriterContent(
    private val body: suspend ByteWriteChannel.() -> Unit,
    override val contentType: ContentType,
    override val status: HttpStatusCode? = null
) : OutgoingContent.WriteChannelContent() {
    override val contentLength: Long? = null
    override val headers: Headers = Headers.Empty
    
    override suspend fun writeTo(channel: ByteWriteChannel)
}

Multipart Data

MultiPartData Interface

interface MultiPartData {
    suspend fun readPart(): PartData?
    
    companion object {
        val Empty: MultiPartData
    }
}

PartData Classes

Base PartData

sealed class PartData {
    abstract val dispose: () -> Unit
    abstract val headers: Headers
    abstract val name: String?
    
    val contentType: ContentType?
    val contentDisposition: ContentDisposition?
}

FormItem

class PartData.FormItem(
    val value: String,
    override val dispose: () -> Unit,
    partHeaders: Headers
) : PartData() {
    override val headers: Headers = partHeaders
    override val name: String? = contentDisposition?.name
}

For simple form fields with string values.

FileItem

class PartData.FileItem(
    val provider: () -> InputStream,
    override val dispose: () -> Unit,
    partHeaders: Headers
) : PartData() {
    override val headers: Headers = partHeaders
    override val name: String? = contentDisposition?.name
    val originalFileName: String? = contentDisposition?.parameter("filename")
}

For file uploads with InputStream access.

BinaryItem

class PartData.BinaryItem(
    val provider: () -> InputStream,
    override val dispose: () -> Unit,
    partHeaders: Headers
) : PartData() {
    override val headers: Headers = partHeaders
    override val name: String? = contentDisposition?.name
}

For binary data with InputStream access.

BinaryChannelItem

class PartData.BinaryChannelItem(
    val provider: () -> ByteReadChannel,
    override val dispose: () -> Unit,
    partHeaders: Headers
) : PartData() {
    override val headers: Headers = partHeaders
    override val name: String? = contentDisposition?.name
}

For binary data with channel-based access.

Content Versioning and Caching

Version Interface

interface Version {
    fun check(requestHeaders: Headers): VersionCheckResult
}

Version Implementations

data class LastModifiedVersion(val lastModified: GMTDate) : Version {
    override fun check(requestHeaders: Headers): VersionCheckResult
}

data class EntityTagVersion(val etag: String) : Version {
    override fun check(requestHeaders: Headers): VersionCheckResult
}

VersionCheckResult

enum class VersionCheckResult {
    OK,                 // Content should be sent
    NOT_MODIFIED,       // Content hasn't changed, send 304
    PRECONDITION_FAILED // Precondition failed, send 412
}

CachingOptions

data class CachingOptions(
    val cacheControl: CacheControl? = null,
    val expires: GMTDate? = null
)

Usage Examples

Basic Content Types

// Text content
val htmlContent = TextContent(
    text = "<html><body><h1>Hello World</h1></body></html>",
    contentType = ContentType.Text.Html.withCharset(Charsets.UTF_8)
)

val jsonContent = TextContent(
    text = """{"message": "Hello, World!", "status": "success"}""",
    contentType = ContentType.Application.Json,
    status = HttpStatusCode.OK
)

// Binary content
val imageBytes = Files.readAllBytes(Paths.get("image.png"))
val imageContent = ByteArrayContent(imageBytes)

// Set content type using wrapper
val typedImageContent = OutgoingContent.ContentWrapper(
    delegate = imageContent,
    contentType = ContentType.Image.PNG,
    contentLength = imageContent.contentLength,
    status = HttpStatusCode.OK,
    headers = headersOf(
        HttpHeaders.CacheControl to "max-age=3600"
    )
)

Streaming Content

// Channel writer content for streaming
val streamingContent = ChannelWriterContent(
    contentType = ContentType.Application.Json,
    body = {
        // Write JSON array with streaming
        writeStringUtf8("[")
        
        repeat(1000) { index ->
            if (index > 0) writeStringUtf8(",")
            writeStringUtf8("""{"id":$index,"name":"Item $index"}""")
            flush() // Flush periodically for streaming
        }
        
        writeStringUtf8("]")
    }
)

// Custom read channel content
class FileReadChannelContent(
    private val file: File,
    override val contentType: ContentType
) : OutgoingContent.ReadChannelContent() {
    
    override val contentLength: Long = file.length()
    override val status: HttpStatusCode? = null
    override val headers: Headers = Headers.Empty
    
    override fun readFrom(): ByteReadChannel {
        return file.readChannel()
    }
}

val fileContent = FileReadChannelContent(
    file = File("large-data.csv"),
    contentType = ContentType.Text.CSV
)

Multipart Form Processing

// Process multipart form data
suspend fun processMultipartData(multipart: MultiPartData) {
    while (true) {
        val part = multipart.readPart() ?: break
        
        try {
            when (part) {
                is PartData.FormItem -> {
                    println("Form field '${part.name}': ${part.value}")
                }
                
                is PartData.FileItem -> {
                    val fileName = part.originalFileName
                    val fieldName = part.name
                    val contentType = part.contentType
                    
                    println("File upload - Field: $fieldName, File: $fileName, Type: $contentType")
                    
                    // Process file content
                    part.provider().use { inputStream ->
                        val fileBytes = inputStream.readBytes()
                        // Save or process file
                        saveUploadedFile(fileName, fileBytes)
                    }
                }
                
                is PartData.BinaryItem -> {
                    println("Binary data - Field: ${part.name}")
                    part.provider().use { inputStream ->
                        val binaryData = inputStream.readBytes()
                        // Process binary data
                        processBinaryData(part.name, binaryData)
                    }
                }
                
                is PartData.BinaryChannelItem -> {
                    println("Binary channel data - Field: ${part.name}")
                    val channel = part.provider()
                    // Process channel data
                    processChannelData(part.name, channel)
                }
            }
        } finally {
            part.dispose() // Always dispose parts
        }
    }
}

fun saveUploadedFile(fileName: String?, bytes: ByteArray) {
    val safeName = fileName?.let { sanitizeFileName(it) } ?: "upload_${System.currentTimeMillis()}"
    File("uploads/$safeName").writeBytes(bytes)
}

fun processBinaryData(fieldName: String?, data: ByteArray) {
    println("Processing ${data.size} bytes for field: $fieldName")
}

suspend fun processChannelData(fieldName: String?, channel: ByteReadChannel) {
    var totalBytes = 0L
    while (!channel.isClosedForRead) {
        val packet = channel.readRemaining(8192)
        totalBytes += packet.remaining
        // Process packet
    }
    println("Processed $totalBytes bytes for field: $fieldName")
}

Content with Caching

// Content with Last-Modified versioning
val lastModified = GMTDate(File("data.json").lastModified())
val versionedContent = object : OutgoingContent() {
    override val contentType = ContentType.Application.Json
    override val contentLength = null
    override val status = null
    override val headers = headersOf(
        HttpHeaders.LastModified to lastModified.toHttpDate()
    )
    
    fun checkVersion(requestHeaders: Headers): VersionCheckResult {
        val version = LastModifiedVersion(lastModified)
        return version.check(requestHeaders)
    }
}

// Content with ETag versioning  
fun createETaggedContent(data: String): OutgoingContent {
    val etag = data.hashCode().toString()
    
    return object : OutgoingContent() {
        override val contentType = ContentType.Application.Json
        override val contentLength = data.toByteArray().size.toLong()
        override val status = null
        override val headers = headersOf(
            HttpHeaders.ETag to "\"$etag\""
        )
        
        fun checkVersion(requestHeaders: Headers): VersionCheckResult {
            val version = EntityTagVersion(etag)
            return version.check(requestHeaders)
        }
        
        fun getContent(): String = data
    }
}

// Usage in request handling
fun handleCachedRequest(content: OutgoingContent, requestHeaders: Headers): OutgoingContent {
    return when (content.checkVersion(requestHeaders)) {
        VersionCheckResult.NOT_MODIFIED -> {
            OutgoingContent.NoContent().apply {
                status = HttpStatusCode.NotModified
            }
        }
        VersionCheckResult.PRECONDITION_FAILED -> {
            OutgoingContent.NoContent().apply {
                status = HttpStatusCode.PreconditionFailed
            }
        }
        VersionCheckResult.OK -> content
    }
}

Advanced Content Patterns

// Content factory with compression
object ContentFactory {
    fun createCompressedJson(data: Any): OutgoingContent {
        val jsonString = Json.encodeToString(data)
        val compressedBytes = gzip(jsonString.toByteArray())
        
        return OutgoingContent.ContentWrapper(
            delegate = ByteArrayContent(compressedBytes),
            contentType = ContentType.Application.Json,
            contentLength = compressedBytes.size.toLong(),
            status = HttpStatusCode.OK,
            headers = headersOf(
                HttpHeaders.ContentEncoding to "gzip"
            )
        )
    }
    
    fun createStreamingCsv(data: List<Map<String, Any>>): OutgoingContent {
        return ChannelWriterContent(
            contentType = ContentType.Text.CSV.withCharset(Charsets.UTF_8),
            body = {
                // Write CSV header
                if (data.isNotEmpty()) {
                    val headers = data.first().keys.joinToString(",")
                    writeStringUtf8("$headers\n")
                }
                
                // Write CSV rows
                data.forEach { row ->
                    val csvRow = row.values.joinToString(",") { value ->
                        "\"${value.toString().replace("\"", "\"\"")}\""
                    }
                    writeStringUtf8("$csvRow\n")
                    flush()
                }
            }
        )
    }
    
    private fun gzip(data: ByteArray): ByteArray {
        return ByteArrayOutputStream().use { baos ->
            GZIPOutputStream(baos).use { gzip ->
                gzip.write(data)
            }
            baos.toByteArray()
        }
    }
}

// Content negotiation helper
class ContentNegotiator {
    fun negotiate(acceptHeader: String?, data: Any): OutgoingContent {
        val acceptedTypes = parseAcceptHeader(acceptHeader)
        
        return when {
            acceptedTypes.any { it.match(ContentType.Application.Json) } ->
                createJsonContent(data)
            acceptedTypes.any { it.match(ContentType.Application.Xml) } ->
                createXmlContent(data)
            acceptedTypes.any { it.match(ContentType.Text.Html) } ->
                createHtmlContent(data)
            else ->
                createJsonContent(data) // Default fallback
        }
    }
    
    private fun parseAcceptHeader(acceptHeader: String?): List<ContentType> {
        return acceptHeader?.split(",")?.mapNotNull { type ->
            try {
                ContentType.parse(type.trim().split(";").first())
            } catch (e: Exception) {
                null
            }
        } ?: listOf(ContentType.Application.Json)
    }
    
    private fun createJsonContent(data: Any): OutgoingContent {
        return TextContent(
            text = Json.encodeToString(data),
            contentType = ContentType.Application.Json
        )
    }
    
    private fun createXmlContent(data: Any): OutgoingContent {
        return TextContent(
            text = convertToXml(data),
            contentType = ContentType.Application.Xml
        )
    }
    
    private fun createHtmlContent(data: Any): OutgoingContent {
        return TextContent(
            text = convertToHtml(data),
            contentType = ContentType.Text.Html.withCharset(Charsets.UTF_8)
        )
    }
}

Error Handling

// Safe multipart processing with error handling
suspend fun processMultipartSafely(multipart: MultiPartData): List<ProcessedPart> {
    val results = mutableListOf<ProcessedPart>()
    
    try {
        while (true) {
            val part = multipart.readPart() ?: break
            
            try {
                val processed = when (part) {
                    is PartData.FormItem -> ProcessedPart.Field(part.name ?: "unknown", part.value)
                    is PartData.FileItem -> processFilePart(part)
                    is PartData.BinaryItem -> processBinaryPart(part)
                    is PartData.BinaryChannelItem -> processChannelPart(part)
                }
                results.add(processed)
            } catch (e: Exception) {
                results.add(ProcessedPart.Error(part.name ?: "unknown", e.message ?: "Unknown error"))
            } finally {
                part.dispose()
            }
        }
    } catch (e: Exception) {
        // Handle multipart parsing errors
        results.add(ProcessedPart.Error("multipart", "Failed to parse multipart data: ${e.message}"))
    }
    
    return results
}

sealed class ProcessedPart {
    data class Field(val name: String, val value: String) : ProcessedPart()
    data class File(val name: String, val fileName: String?, val size: Long) : ProcessedPart()
    data class Binary(val name: String, val size: Long) : ProcessedPart()
    data class Error(val name: String, val message: String) : ProcessedPart()
}

fun processFilePart(part: PartData.FileItem): ProcessedPart.File {
    return try {
        part.provider().use { inputStream ->
            val bytes = inputStream.readBytes()
            // Save file or process
            ProcessedPart.File(
                name = part.name ?: "unknown",
                fileName = part.originalFileName,
                size = bytes.size.toLong()
            )
        }
    } catch (e: Exception) {
        throw RuntimeException("Failed to process file: ${e.message}", e)
    }
}