Skip to content

Commit cc81b68

Browse files
Gleb NazarovGleb Nazarov
authored andcommitted
KTOR-7470 Rethrow UnsupportedMediaTypeException if content type header is not present or is invalid
1 parent 68d447c commit cc81b68

File tree

3 files changed

+75
-11
lines changed

3 files changed

+75
-11
lines changed

ktor-server/ktor-server-core/common/src/io/ktor/server/plugins/Errors.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ public class CannotTransformContentToTypeException(
8585
*/
8686
@OptIn(ExperimentalCoroutinesApi::class)
8787
public class UnsupportedMediaTypeException(
88-
private val contentType: ContentType
89-
) : ContentTransformationException("Content type $contentType is not supported"),
90-
CopyableThrowable<UnsupportedMediaTypeException> {
88+
private val contentType: ContentType?
89+
) : ContentTransformationException(
90+
contentType?.let { "Content type $it is not supported" }
91+
?: "Content-Type header is required for multipart processing"
92+
), CopyableThrowable<UnsupportedMediaTypeException> {
9193

9294
override fun createCopy(): UnsupportedMediaTypeException = UnsupportedMediaTypeException(contentType).also {
9395
it.initCauseBridge(this)

ktor-server/ktor-server-core/jvm/src/io/ktor/server/engine/DefaultTransformJvm.kt

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.ktor.http.*
88
import io.ktor.http.cio.*
99
import io.ktor.http.content.*
1010
import io.ktor.server.application.*
11+
import io.ktor.server.plugins.UnsupportedMediaTypeException
1112
import io.ktor.server.request.*
1213
import io.ktor.util.pipeline.*
1314
import io.ktor.utils.io.*
@@ -17,6 +18,7 @@ import io.ktor.utils.io.streams.*
1718
import kotlinx.coroutines.*
1819
import kotlinx.io.*
1920
import java.io.*
21+
import java.io.IOException
2022

2123
internal actual suspend fun PipelineContext<Any, PipelineCall>.defaultPlatformTransformations(
2224
query: Any
@@ -33,16 +35,21 @@ internal actual suspend fun PipelineContext<Any, PipelineCall>.defaultPlatformTr
3335
@OptIn(InternalAPI::class)
3436
internal actual fun PipelineContext<*, PipelineCall>.multiPartData(rc: ByteReadChannel): MultiPartData {
3537
val contentType = call.request.header(HttpHeaders.ContentType)
36-
?: throw IllegalStateException("Content-Type header is required for multipart processing")
38+
?: throw UnsupportedMediaTypeException(null)
3739

3840
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLong()
39-
return CIOMultipartDataBase(
40-
coroutineContext + Dispatchers.Unconfined,
41-
rc,
42-
contentType,
43-
contentLength,
44-
call.formFieldLimit
45-
)
41+
42+
try {
43+
return CIOMultipartDataBase(
44+
coroutineContext + Dispatchers.Unconfined,
45+
rc,
46+
contentType,
47+
contentLength,
48+
call.formFieldLimit
49+
)
50+
} catch (_: IOException) {
51+
throw UnsupportedMediaTypeException(ContentType.parse(contentType))
52+
}
4653
}
4754

4855
internal actual fun Source.readTextWithCustomCharset(charset: Charset): String =
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.tests.server.engine
6+
7+
import io.ktor.http.*
8+
import io.ktor.server.application.*
9+
import io.ktor.server.engine.multiPartData
10+
import io.ktor.server.plugins.UnsupportedMediaTypeException
11+
import io.ktor.server.request.*
12+
import io.ktor.util.pipeline.*
13+
import io.ktor.utils.io.*
14+
import io.mockk.*
15+
import kotlinx.coroutines.*
16+
import kotlinx.coroutines.test.TestScope
17+
import kotlinx.coroutines.test.runTest
18+
import kotlin.test.*
19+
20+
class MultiPartDataTest {
21+
private val mockContext = mockk<PipelineContext<*, PipelineCall>>(relaxed = true)
22+
private val mockRequest = mockk<PipelineRequest>(relaxed = true)
23+
private val testScope = TestScope()
24+
25+
@Test
26+
fun givenRequest_whenNoContentTypeHeaderPresent_thenUnsupportedMediaTypeException() {
27+
// Setup
28+
every { mockContext.call.request } returns mockRequest
29+
every { mockRequest.header(HttpHeaders.ContentType) } returns null
30+
31+
// Act & Assert
32+
assertFailsWith<UnsupportedMediaTypeException> {
33+
runBlocking { mockContext.multiPartData(ByteReadChannel("sample data")) }
34+
}
35+
}
36+
37+
@Test
38+
fun givenWrongContentType_whenProcessMultiPart_thenUnsupportedMediaTypeException() {
39+
// Given
40+
val rc = ByteReadChannel("sample data")
41+
val contentType = "test/plain; boundary=test"
42+
val contentLength = "123"
43+
every { mockContext.call.request } returns mockRequest
44+
every { mockContext.call.attributes.getOrNull<Long>(any()) } returns 0L
45+
every { mockRequest.header(HttpHeaders.ContentType) } returns contentType
46+
every { mockRequest.header(HttpHeaders.ContentLength) } returns contentLength
47+
48+
// When & Then
49+
testScope.runTest {
50+
assertFailsWith<UnsupportedMediaTypeException> {
51+
mockContext.multiPartData(rc)
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)