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.
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?
}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.
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
}abstract class OutgoingContent.ReadChannelContent : OutgoingContent() {
abstract fun readFrom(): ByteReadChannel
}For streaming content that can be read from a channel.
abstract class OutgoingContent.WriteChannelContent : OutgoingContent() {
abstract suspend fun writeTo(channel: ByteWriteChannel)
}For content that writes to a channel.
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.
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.
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
}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
}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)
}interface MultiPartData {
suspend fun readPart(): PartData?
companion object {
val Empty: MultiPartData
}
}sealed class PartData {
abstract val dispose: () -> Unit
abstract val headers: Headers
abstract val name: String?
val contentType: ContentType?
val contentDisposition: ContentDisposition?
}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.
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.
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.
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.
interface Version {
fun check(requestHeaders: Headers): VersionCheckResult
}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
}enum class VersionCheckResult {
OK, // Content should be sent
NOT_MODIFIED, // Content hasn't changed, send 304
PRECONDITION_FAILED // Precondition failed, send 412
}data class CachingOptions(
val cacheControl: CacheControl? = null,
val expires: GMTDate? = null
)// 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"
)
)// 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
)// 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 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
}
}// 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)
)
}
}// 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)
}
}