or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

authentication.mdcontent-handling.mdcontent-types.mdcookie-management.mdheaders-parameters.mdhttp-core-types.mdindex.mdmessage-properties.mdmultipart-data.mdurl-encoding.mdurl-handling.md

multipart-data.mddocs/

0

# Multipart Data

1

2

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

3

4

## Capabilities

5

6

### MultiPartData Interface

7

8

Interface for reading multipart data streams.

9

10

```kotlin { .api }

11

/**

12

* Interface for reading multipart data

13

*/

14

interface MultiPartData {

15

/**

16

* Read next part from multipart stream

17

* @return PartData instance or null if no more parts

18

*/

19

suspend fun readPart(): PartData?

20

21

object Empty : MultiPartData {

22

override suspend fun readPart(): PartData? = null

23

}

24

}

25

```

26

27

### PartData Base Class

28

29

Base class for individual parts in multipart data.

30

31

```kotlin { .api }

32

/**

33

* Base class for multipart data parts

34

*/

35

abstract class PartData {

36

/**

37

* Part headers

38

*/

39

abstract val headers: Headers

40

41

/**

42

* Function to dispose/cleanup part resources

43

*/

44

abstract val dispose: () -> Unit

45

46

/**

47

* Content-Disposition header parsed

48

*/

49

val contentDisposition: ContentDisposition?

50

51

/**

52

* Content-Type header parsed

53

*/

54

val contentType: ContentType?

55

56

/**

57

* Part name from Content-Disposition

58

*/

59

val name: String?

60

}

61

```

62

63

### Form Item Part

64

65

Text form field part data.

66

67

```kotlin { .api }

68

/**

69

* Form field part with text value

70

*/

71

class PartData.FormItem(

72

val value: String,

73

dispose: () -> Unit,

74

partHeaders: Headers

75

) : PartData {

76

77

override val headers: Headers = partHeaders

78

override val dispose: () -> Unit = dispose

79

}

80

```

81

82

### Binary Item Part

83

84

Binary data part with input stream provider.

85

86

```kotlin { .api }

87

/**

88

* Binary data part with input stream

89

*/

90

class PartData.BinaryItem(

91

val provider: () -> InputStream,

92

dispose: () -> Unit,

93

partHeaders: Headers

94

) : PartData {

95

96

override val headers: Headers = partHeaders

97

override val dispose: () -> Unit = dispose

98

}

99

```

100

101

### File Item Part

102

103

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

104

105

```kotlin { .api }

106

/**

107

* File upload part with filename

108

*/

109

class PartData.FileItem(

110

val provider: () -> InputStream,

111

dispose: () -> Unit,

112

partHeaders: Headers

113

) : PartData {

114

115

override val headers: Headers = partHeaders

116

override val dispose: () -> Unit = dispose

117

118

/**

119

* Original filename from Content-Disposition

120

*/

121

val originalFileName: String?

122

123

/**

124

* JVM-specific stream provider

125

*/

126

val streamProvider: () -> InputStream

127

}

128

```

129

130

### Binary Channel Item Part

131

132

Binary data part with channel-based access.

133

134

```kotlin { .api }

135

/**

136

* Binary data part with channel access

137

*/

138

class PartData.BinaryChannelItem(

139

val provider: () -> ByteReadChannel,

140

dispose: () -> Unit,

141

partHeaders: Headers

142

) : PartData {

143

144

override val headers: Headers = partHeaders

145

override val dispose: () -> Unit = dispose

146

}

147

```

148

149

### Content Disposition

150

151

Content-Disposition header parsing and representation.

152

153

```kotlin { .api }

154

/**

155

* Content-Disposition header representation

156

*/

157

class ContentDisposition private constructor(

158

val disposition: String,

159

parameters: List<HeaderValueParam> = emptyList()

160

) : HeaderValueWithParameters {

161

162

/**

163

* Disposition name parameter

164

*/

165

val name: String?

166

167

/**

168

* Add parameter to Content-Disposition

169

* @param name parameter name

170

* @param value parameter value

171

* @param escapeValue whether to escape the value

172

* @return new ContentDisposition with added parameter

173

*/

174

fun withParameter(name: String, value: String, escapeValue: Boolean = false): ContentDisposition

175

176

/**

177

* Replace parameters

178

* @param parameters new parameter list

179

* @return new ContentDisposition with replaced parameters

180

*/

181

fun withParameters(parameters: List<HeaderValueParam>): ContentDisposition

182

183

companion object {

184

val Inline: ContentDisposition

185

val Attachment: ContentDisposition

186

val File: ContentDisposition

187

val Mixed: ContentDisposition

188

189

/**

190

* Parse Content-Disposition header

191

* @param value header value

192

* @return ContentDisposition instance

193

*/

194

fun parse(value: String): ContentDisposition

195

}

196

197

object Parameters {

198

const val Name: String = "name"

199

const val FileName: String = "filename"

200

const val FileNameAsterisk: String = "filename*"

201

const val CreationDate: String = "creation-date"

202

const val ModificationDate: String = "modification-date"

203

const val ReadDate: String = "read-date"

204

const val Size: String = "size"

205

const val Handling: String = "handling"

206

}

207

}

208

```

209

210

### Multipart Processing Utilities

211

212

Utility functions for processing multipart data streams.

213

214

```kotlin { .api }

215

/**

216

* Convert MultiPartData to Flow

217

* @return Flow of PartData instances

218

*/

219

fun MultiPartData.asFlow(): Flow<PartData>

220

221

/**

222

* Process each part with a handler function

223

* @param handler function to process each part

224

*/

225

suspend fun MultiPartData.forEachPart(handler: suspend (PartData) -> Unit)

226

227

/**

228

* Read all parts into a list

229

* @return List of all PartData instances

230

*/

231

suspend fun MultiPartData.readAllParts(): List<PartData>

232

```

233

234

**Usage Examples:**

235

236

```kotlin

237

import io.ktor.http.*

238

import io.ktor.http.content.*

239

import kotlinx.coroutines.flow.*

240

import java.io.*

241

242

// Processing multipart data

243

suspend fun processMultipartData(multipart: MultiPartData) {

244

multipart.forEachPart { part ->

245

when (part) {

246

is PartData.FormItem -> {

247

println("Form field '${part.name}': ${part.value}")

248

}

249

250

is PartData.FileItem -> {

251

println("File upload '${part.name}': ${part.originalFileName}")

252

println("Content-Type: ${part.contentType}")

253

254

// JVM-specific: Access file content via InputStream

255

part.streamProvider().use { inputStream ->

256

val bytes = inputStream.readBytes()

257

println("File size: ${bytes.size} bytes")

258

259

// Process file content

260

saveUploadedFile(part.originalFileName ?: "unknown", bytes)

261

}

262

}

263

264

is PartData.BinaryItem -> {

265

println("Binary data '${part.name}'")

266

part.provider().use { inputStream ->

267

// Process binary data

268

val content = inputStream.readBytes()

269

processBinaryContent(content)

270

}

271

}

272

273

is PartData.BinaryChannelItem -> {

274

println("Binary channel data '${part.name}'")

275

val channel = part.provider()

276

// Read from channel

277

val content = channel.readRemaining().readBytes()

278

processBinaryContent(content)

279

}

280

}

281

282

// Always dispose part resources

283

part.dispose()

284

}

285

}

286

287

// Using Flow API for streaming processing

288

suspend fun processMultipartStream(multipart: MultiPartData) {

289

multipart.asFlow()

290

.collect { part ->

291

when (part) {

292

is PartData.FileItem -> {

293

// Stream process large files

294

part.streamProvider().use { stream ->

295

val buffered = stream.buffered()

296

val buffer = ByteArray(8192)

297

var totalBytes = 0L

298

299

while (true) {

300

val bytesRead = buffered.read(buffer)

301

if (bytesRead == -1) break

302

303

totalBytes += bytesRead

304

// Process chunk

305

processFileChunk(buffer, bytesRead)

306

}

307

308

println("Processed $totalBytes bytes from ${part.originalFileName}")

309

}

310

}

311

else -> {

312

// Handle other part types

313

}

314

}

315

part.dispose()

316

}

317

}

318

319

// Reading all parts at once (for smaller datasets)

320

suspend fun getAllParts(multipart: MultiPartData): List<PartData> {

321

return multipart.readAllParts()

322

}

323

324

// Working with Content-Disposition

325

fun parseContentDisposition() {

326

val dispositionValue = "form-data; name=\"file\"; filename=\"document.pdf\""

327

val disposition = ContentDisposition.parse(dispositionValue)

328

329

println("Disposition: ${disposition.disposition}") // form-data

330

println("Name: ${disposition.name}") // file

331

println("Parameter: ${disposition.parameter(ContentDisposition.Parameters.FileName)}") // document.pdf

332

333

// Create Content-Disposition

334

val fileDisposition = ContentDisposition.Attachment

335

.withParameter(ContentDisposition.Parameters.FileName, "report.xlsx")

336

.withParameter(ContentDisposition.Parameters.Size, "1024")

337

338

println(fileDisposition.toString()) // attachment; filename="report.xlsx"; size="1024"

339

}

340

341

// Form data validation and processing

342

suspend fun validateAndProcessForm(multipart: MultiPartData): Map<String, Any> {

343

val formData = mutableMapOf<String, Any>()

344

val uploadedFiles = mutableListOf<UploadedFile>()

345

346

multipart.forEachPart { part ->

347

when (part) {

348

is PartData.FormItem -> {

349

val fieldName = part.name ?: "unknown"

350

351

// Validate form fields

352

when (fieldName) {

353

"email" -> {

354

if (part.value.contains("@")) {

355

formData[fieldName] = part.value

356

} else {

357

throw IllegalArgumentException("Invalid email format")

358

}

359

}

360

"age" -> {

361

val age = part.value.toIntOrNull()

362

if (age != null && age in 1..120) {

363

formData[fieldName] = age

364

} else {

365

throw IllegalArgumentException("Invalid age")

366

}

367

}

368

else -> {

369

formData[fieldName] = part.value

370

}

371

}

372

}

373

374

is PartData.FileItem -> {

375

val fileName = part.originalFileName

376

val contentType = part.contentType

377

378

// Validate file upload

379

if (fileName.isNullOrBlank()) {

380

throw IllegalArgumentException("Filename is required")

381

}

382

383

if (contentType?.match(ContentType.Image.Any) != true) {

384

throw IllegalArgumentException("Only image files allowed")

385

}

386

387

// Save file

388

val tempFile = createTempFile(prefix = "upload_", suffix = ".tmp")

389

part.streamProvider().use { input ->

390

tempFile.outputStream().use { output ->

391

input.copyTo(output)

392

}

393

}

394

395

uploadedFiles.add(UploadedFile(fileName, tempFile, contentType))

396

}

397

}

398

part.dispose()

399

}

400

401

formData["uploadedFiles"] = uploadedFiles

402

return formData

403

}

404

405

// Data classes for structured processing

406

data class UploadedFile(

407

val originalName: String,

408

val tempFile: File,

409

val contentType: ContentType?

410

)

411

412

// Helper functions

413

fun saveUploadedFile(filename: String, content: ByteArray) {

414

val file = File("uploads", filename)

415

file.parentFile.mkdirs()

416

file.writeBytes(content)

417

}

418

419

fun processBinaryContent(content: ByteArray) {

420

println("Processing ${content.size} bytes of binary data")

421

// Process binary content

422

}

423

424

fun processFileChunk(buffer: ByteArray, size: Int) {

425

// Process file chunk

426

println("Processing chunk of $size bytes")

427

}

428

429

// Error handling in multipart processing

430

suspend fun safeProcessMultipart(multipart: MultiPartData) {

431

try {

432

var partCount = 0

433

multipart.forEachPart { part ->

434

partCount++

435

436

try {

437

when (part) {

438

is PartData.FileItem -> {

439

if (part.originalFileName?.endsWith(".exe") == true) {

440

throw SecurityException("Executable files not allowed")

441

}

442

// Process file safely

443

}

444

is PartData.FormItem -> {

445

if (part.value.length > 10_000) {

446

throw IllegalArgumentException("Form field too large")

447

}

448

// Process form field

449

}

450

}

451

} catch (e: Exception) {

452

println("Error processing part $partCount: ${e.message}")

453

// Continue with next part

454

} finally {

455

// Always dispose resources

456

part.dispose()

457

}

458

}

459

} catch (e: Exception) {

460

println("Error reading multipart data: ${e.message}")

461

}

462

}

463

464

// Async processing with limits

465

suspend fun processMultipartWithLimits(

466

multipart: MultiPartData,

467

maxParts: Int = 100,

468

maxPartSize: Long = 10 * 1024 * 1024 // 10MB

469

) {

470

var partCount = 0

471

472

multipart.asFlow()

473

.take(maxParts)

474

.collect { part ->

475

partCount++

476

477

when (part) {

478

is PartData.FileItem -> {

479

part.streamProvider().use { stream ->

480

val limitedStream = stream.buffered()

481

var totalRead = 0L

482

val buffer = ByteArray(8192)

483

484

while (totalRead < maxPartSize) {

485

val bytesRead = limitedStream.read(buffer)

486

if (bytesRead == -1) break

487

488

totalRead += bytesRead

489

490

if (totalRead > maxPartSize) {

491

throw IllegalArgumentException("Part exceeds size limit")

492

}

493

494

// Process chunk

495

}

496

}

497

}

498

}

499

500

part.dispose()

501

}

502

}

503

```

504

505

## Types

506

507

All types are defined above in their respective capability sections.