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
—
Multipart form data processing with support for file uploads, form fields, and streaming multipart content with JVM-specific stream providers.
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
}
}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?
}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 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 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 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 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"
}
}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()
}
}All types are defined above in their respective capability sections.
Install with Tessl CLI
npx tessl i tessl/maven-io-ktor--ktor-http-jvm