CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-ktor--ktor-http-jvm

JVM-specific HTTP utilities and extensions for the Ktor framework providing URL utilities, file content type detection, HTTP message properties, and content handling for JVM platforms

Pending
Overview
Eval results
Files

multipart-data.mddocs/

Multipart Data

Multipart form data processing with support for file uploads, form fields, and streaming multipart content with JVM-specific stream providers.

Capabilities

MultiPartData Interface

Interface for reading multipart data streams.

/**
 * Interface for reading multipart data
 */
interface MultiPartData {
    /**
     * Read next part from multipart stream
     * @return PartData instance or null if no more parts
     */
    suspend fun readPart(): PartData?
    
    object Empty : MultiPartData {
        override suspend fun readPart(): PartData? = null
    }
}

PartData Base Class

Base class for individual parts in multipart data.

/**
 * Base class for multipart data parts
 */
abstract class PartData {
    /**
     * Part headers
     */
    abstract val headers: Headers
    
    /**
     * Function to dispose/cleanup part resources
     */
    abstract val dispose: () -> Unit
    
    /**
     * Content-Disposition header parsed
     */
    val contentDisposition: ContentDisposition?
    
    /**
     * Content-Type header parsed
     */
    val contentType: ContentType?
    
    /**
     * Part name from Content-Disposition
     */
    val name: String?
}

Form Item Part

Text form field part data.

/**
 * Form field part with text value
 */
class PartData.FormItem(
    val value: String,
    dispose: () -> Unit,
    partHeaders: Headers
) : PartData {
    
    override val headers: Headers = partHeaders
    override val dispose: () -> Unit = dispose
}

Binary Item Part

Binary data part with input stream provider.

/**
 * Binary data part with input stream
 */
class PartData.BinaryItem(
    val provider: () -> InputStream,
    dispose: () -> Unit,
    partHeaders: Headers
) : PartData {
    
    override val headers: Headers = partHeaders
    override val dispose: () -> Unit = dispose
}

File Item Part

File upload part with original filename and JVM-specific stream provider.

/**
 * File upload part with filename
 */
class PartData.FileItem(
    val provider: () -> InputStream,
    dispose: () -> Unit,
    partHeaders: Headers
) : PartData {
    
    override val headers: Headers = partHeaders
    override val dispose: () -> Unit = dispose
    
    /**
     * Original filename from Content-Disposition
     */
    val originalFileName: String?
    
    /**
     * JVM-specific stream provider
     */
    val streamProvider: () -> InputStream
}

Binary Channel Item Part

Binary data part with channel-based access.

/**
 * Binary data part with channel access
 */
class PartData.BinaryChannelItem(
    val provider: () -> ByteReadChannel,
    dispose: () -> Unit,
    partHeaders: Headers
) : PartData {
    
    override val headers: Headers = partHeaders
    override val dispose: () -> Unit = dispose
}

Content Disposition

Content-Disposition header parsing and representation.

/**
 * Content-Disposition header representation
 */
class ContentDisposition private constructor(
    val disposition: String,
    parameters: List<HeaderValueParam> = emptyList()
) : HeaderValueWithParameters {
    
    /**
     * Disposition name parameter
     */
    val name: String?
    
    /**
     * Add parameter to Content-Disposition
     * @param name parameter name
     * @param value parameter value
     * @param escapeValue whether to escape the value
     * @return new ContentDisposition with added parameter
     */
    fun withParameter(name: String, value: String, escapeValue: Boolean = false): ContentDisposition
    
    /**
     * Replace parameters
     * @param parameters new parameter list
     * @return new ContentDisposition with replaced parameters
     */
    fun withParameters(parameters: List<HeaderValueParam>): ContentDisposition
    
    companion object {
        val Inline: ContentDisposition
        val Attachment: ContentDisposition
        val File: ContentDisposition
        val Mixed: ContentDisposition
        
        /**
         * Parse Content-Disposition header
         * @param value header value
         * @return ContentDisposition instance
         */
        fun parse(value: String): ContentDisposition
    }
    
    object Parameters {
        const val Name: String = "name"
        const val FileName: String = "filename"
        const val FileNameAsterisk: String = "filename*"
        const val CreationDate: String = "creation-date"
        const val ModificationDate: String = "modification-date"
        const val ReadDate: String = "read-date"
        const val Size: String = "size"
        const val Handling: String = "handling"
    }
}

Multipart Processing Utilities

Utility functions for processing multipart data streams.

/**
 * Convert MultiPartData to Flow
 * @return Flow of PartData instances
 */
fun MultiPartData.asFlow(): Flow<PartData>

/**
 * Process each part with a handler function
 * @param handler function to process each part
 */
suspend fun MultiPartData.forEachPart(handler: suspend (PartData) -> Unit)

/**
 * Read all parts into a list
 * @return List of all PartData instances
 */
suspend fun MultiPartData.readAllParts(): List<PartData>

Usage Examples:

import io.ktor.http.*
import io.ktor.http.content.*
import kotlinx.coroutines.flow.*
import java.io.*

// Processing multipart data
suspend fun processMultipartData(multipart: MultiPartData) {
    multipart.forEachPart { part ->
        when (part) {
            is PartData.FormItem -> {
                println("Form field '${part.name}': ${part.value}")
            }
            
            is PartData.FileItem -> {
                println("File upload '${part.name}': ${part.originalFileName}")
                println("Content-Type: ${part.contentType}")
                
                // JVM-specific: Access file content via InputStream
                part.streamProvider().use { inputStream ->
                    val bytes = inputStream.readBytes()
                    println("File size: ${bytes.size} bytes")
                    
                    // Process file content
                    saveUploadedFile(part.originalFileName ?: "unknown", bytes)
                }
            }
            
            is PartData.BinaryItem -> {
                println("Binary data '${part.name}'")
                part.provider().use { inputStream ->
                    // Process binary data
                    val content = inputStream.readBytes()
                    processBinaryContent(content)
                }
            }
            
            is PartData.BinaryChannelItem -> {
                println("Binary channel data '${part.name}'")
                val channel = part.provider()
                // Read from channel
                val content = channel.readRemaining().readBytes()
                processBinaryContent(content)
            }
        }
        
        // Always dispose part resources
        part.dispose()
    }
}

// Using Flow API for streaming processing
suspend fun processMultipartStream(multipart: MultiPartData) {
    multipart.asFlow()
        .collect { part ->
            when (part) {
                is PartData.FileItem -> {
                    // Stream process large files
                    part.streamProvider().use { stream ->
                        val buffered = stream.buffered()
                        val buffer = ByteArray(8192)
                        var totalBytes = 0L
                        
                        while (true) {
                            val bytesRead = buffered.read(buffer)
                            if (bytesRead == -1) break
                            
                            totalBytes += bytesRead
                            // Process chunk
                            processFileChunk(buffer, bytesRead)
                        }
                        
                        println("Processed $totalBytes bytes from ${part.originalFileName}")
                    }
                }
                else -> {
                    // Handle other part types
                }
            }
            part.dispose()
        }
}

// Reading all parts at once (for smaller datasets)
suspend fun getAllParts(multipart: MultiPartData): List<PartData> {
    return multipart.readAllParts()
}

// Working with Content-Disposition
fun parseContentDisposition() {
    val dispositionValue = "form-data; name=\"file\"; filename=\"document.pdf\""
    val disposition = ContentDisposition.parse(dispositionValue)
    
    println("Disposition: ${disposition.disposition}") // form-data
    println("Name: ${disposition.name}") // file
    println("Parameter: ${disposition.parameter(ContentDisposition.Parameters.FileName)}") // document.pdf
    
    // Create Content-Disposition
    val fileDisposition = ContentDisposition.Attachment
        .withParameter(ContentDisposition.Parameters.FileName, "report.xlsx")
        .withParameter(ContentDisposition.Parameters.Size, "1024")
    
    println(fileDisposition.toString()) // attachment; filename="report.xlsx"; size="1024"
}

// Form data validation and processing
suspend fun validateAndProcessForm(multipart: MultiPartData): Map<String, Any> {
    val formData = mutableMapOf<String, Any>()
    val uploadedFiles = mutableListOf<UploadedFile>()
    
    multipart.forEachPart { part ->
        when (part) {
            is PartData.FormItem -> {
                val fieldName = part.name ?: "unknown"
                
                // Validate form fields
                when (fieldName) {
                    "email" -> {
                        if (part.value.contains("@")) {
                            formData[fieldName] = part.value
                        } else {
                            throw IllegalArgumentException("Invalid email format")
                        }
                    }
                    "age" -> {
                        val age = part.value.toIntOrNull()
                        if (age != null && age in 1..120) {
                            formData[fieldName] = age
                        } else {
                            throw IllegalArgumentException("Invalid age")
                        }
                    }
                    else -> {
                        formData[fieldName] = part.value
                    }
                }
            }
            
            is PartData.FileItem -> {
                val fileName = part.originalFileName
                val contentType = part.contentType
                
                // Validate file upload
                if (fileName.isNullOrBlank()) {
                    throw IllegalArgumentException("Filename is required")
                }
                
                if (contentType?.match(ContentType.Image.Any) != true) {
                    throw IllegalArgumentException("Only image files allowed")
                }
                
                // Save file
                val tempFile = createTempFile(prefix = "upload_", suffix = ".tmp")
                part.streamProvider().use { input ->
                    tempFile.outputStream().use { output ->
                        input.copyTo(output)
                    }
                }
                
                uploadedFiles.add(UploadedFile(fileName, tempFile, contentType))
            }
        }
        part.dispose()
    }
    
    formData["uploadedFiles"] = uploadedFiles
    return formData
}

// Data classes for structured processing
data class UploadedFile(
    val originalName: String,
    val tempFile: File,
    val contentType: ContentType?
)

// Helper functions
fun saveUploadedFile(filename: String, content: ByteArray) {
    val file = File("uploads", filename)
    file.parentFile.mkdirs()
    file.writeBytes(content)
}

fun processBinaryContent(content: ByteArray) {
    println("Processing ${content.size} bytes of binary data")
    // Process binary content
}

fun processFileChunk(buffer: ByteArray, size: Int) {
    // Process file chunk
    println("Processing chunk of $size bytes")
}

// Error handling in multipart processing
suspend fun safeProcessMultipart(multipart: MultiPartData) {
    try {
        var partCount = 0
        multipart.forEachPart { part ->
            partCount++
            
            try {
                when (part) {
                    is PartData.FileItem -> {
                        if (part.originalFileName?.endsWith(".exe") == true) {
                            throw SecurityException("Executable files not allowed")
                        }
                        // Process file safely
                    }
                    is PartData.FormItem -> {
                        if (part.value.length > 10_000) {
                            throw IllegalArgumentException("Form field too large")
                        }
                        // Process form field
                    }
                }
            } catch (e: Exception) {
                println("Error processing part $partCount: ${e.message}")
                // Continue with next part
            } finally {
                // Always dispose resources
                part.dispose()
            }
        }
    } catch (e: Exception) {
        println("Error reading multipart data: ${e.message}")
    }
}

// Async processing with limits
suspend fun processMultipartWithLimits(
    multipart: MultiPartData,
    maxParts: Int = 100,
    maxPartSize: Long = 10 * 1024 * 1024 // 10MB
) {
    var partCount = 0
    
    multipart.asFlow()
        .take(maxParts)
        .collect { part ->
            partCount++
            
            when (part) {
                is PartData.FileItem -> {
                    part.streamProvider().use { stream ->
                        val limitedStream = stream.buffered()
                        var totalRead = 0L
                        val buffer = ByteArray(8192)
                        
                        while (totalRead < maxPartSize) {
                            val bytesRead = limitedStream.read(buffer)
                            if (bytesRead == -1) break
                            
                            totalRead += bytesRead
                            
                            if (totalRead > maxPartSize) {
                                throw IllegalArgumentException("Part exceeds size limit")
                            }
                            
                            // Process chunk
                        }
                    }
                }
            }
            
            part.dispose()
        }
}

Types

All types are defined above in their respective capability sections.

Install with Tessl CLI

npx tessl i tessl/maven-io-ktor--ktor-http-jvm

docs

authentication.md

content-handling.md

content-types.md

cookie-management.md

headers-parameters.md

http-core-types.md

index.md

message-properties.md

multipart-data.md

url-encoding.md

url-handling.md

tile.json