Коммит 7c8b92f6 создал по автору Gleb Nazarov's avatar Gleb Nazarov Зафиксировано автором osip.fatkullin
Просмотр файлов

KTOR-7470 receiveMultipart throw UnsupportedMediaTypeException (#4339)

владелец b7a84046
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
# Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. # Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
# #
# sytleguide # styleguide
kotlin.code.style=official kotlin.code.style=official
# config # config
......
...@@ -155,3 +155,7 @@ public final class io/ktor/http/cio/internals/MutableRange { ...@@ -155,3 +155,7 @@ public final class io/ktor/http/cio/internals/MutableRange {
public fun toString ()Ljava/lang/String; public fun toString ()Ljava/lang/String;
} }
public final class io/ktor/http/cio/internals/UnsupportedMediaTypeExceptionCIO : java/io/IOException {
public fun <init> (Ljava/lang/String;)V
}
...@@ -27,6 +27,10 @@ final class io.ktor.http.cio.internals/MutableRange { // io.ktor.http.cio.intern ...@@ -27,6 +27,10 @@ final class io.ktor.http.cio.internals/MutableRange { // io.ktor.http.cio.intern
final fun toString(): kotlin/String // io.ktor.http.cio.internals/MutableRange.toString|toString(){}[0] final fun toString(): kotlin/String // io.ktor.http.cio.internals/MutableRange.toString|toString(){}[0]
} }
final class io.ktor.http.cio.internals/UnsupportedMediaTypeExceptionCIO : kotlinx.io/IOException { // io.ktor.http.cio.internals/UnsupportedMediaTypeExceptionCIO|null[0]
constructor <init>(kotlin/String) // io.ktor.http.cio.internals/UnsupportedMediaTypeExceptionCIO.<init>|<init>(kotlin.String){}[0]
}
final class io.ktor.http.cio/CIOHeaders : io.ktor.http/Headers { // io.ktor.http.cio/CIOHeaders|null[0] final class io.ktor.http.cio/CIOHeaders : io.ktor.http/Headers { // io.ktor.http.cio/CIOHeaders|null[0]
constructor <init>(io.ktor.http.cio/HttpHeadersMap) // io.ktor.http.cio/CIOHeaders.<init>|<init>(io.ktor.http.cio.HttpHeadersMap){}[0] constructor <init>(io.ktor.http.cio/HttpHeadersMap) // io.ktor.http.cio/CIOHeaders.<init>|<init>(io.ktor.http.cio.HttpHeadersMap){}[0]
......
...@@ -136,12 +136,15 @@ private suspend fun ByteReadChannel.skipIfFoundReadCount(prefix: ByteString): Lo ...@@ -136,12 +136,15 @@ private suspend fun ByteReadChannel.skipIfFoundReadCount(prefix: ByteString): Lo
/** /**
* Starts a multipart parser coroutine producing multipart events * Starts a multipart parser coroutine producing multipart events
*/ */
@OptIn(InternalAPI::class)
public fun CoroutineScope.parseMultipart( public fun CoroutineScope.parseMultipart(
input: ByteReadChannel, input: ByteReadChannel,
headers: HttpHeadersMap, headers: HttpHeadersMap,
maxPartSize: Long = Long.MAX_VALUE maxPartSize: Long = Long.MAX_VALUE
): ReceiveChannel<MultipartEvent> { ): ReceiveChannel<MultipartEvent> {
val contentType = headers["Content-Type"] ?: throw IOException("Failed to parse multipart: no Content-Type header") val contentType = headers["Content-Type"] ?: throw UnsupportedMediaTypeExceptionCIO(
"Failed to parse multipart: no Content-Type header"
)
val contentLength = headers["Content-Length"]?.parseDecLong() val contentLength = headers["Content-Length"]?.parseDecLong()
return parseMultipart(input, contentType, contentLength, maxPartSize) return parseMultipart(input, contentType, contentLength, maxPartSize)
...@@ -150,6 +153,7 @@ public fun CoroutineScope.parseMultipart( ...@@ -150,6 +153,7 @@ public fun CoroutineScope.parseMultipart(
/** /**
* Starts a multipart parser coroutine producing multipart events * Starts a multipart parser coroutine producing multipart events
*/ */
@OptIn(InternalAPI::class)
public fun CoroutineScope.parseMultipart( public fun CoroutineScope.parseMultipart(
input: ByteReadChannel, input: ByteReadChannel,
contentType: CharSequence, contentType: CharSequence,
...@@ -157,7 +161,9 @@ public fun CoroutineScope.parseMultipart( ...@@ -157,7 +161,9 @@ public fun CoroutineScope.parseMultipart(
maxPartSize: Long = Long.MAX_VALUE, maxPartSize: Long = Long.MAX_VALUE,
): ReceiveChannel<MultipartEvent> { ): ReceiveChannel<MultipartEvent> {
if (!contentType.startsWith("multipart/", ignoreCase = true)) { if (!contentType.startsWith("multipart/", ignoreCase = true)) {
throw IOException("Failed to parse multipart: Content-Type should be multipart/* but it is $contentType") throw UnsupportedMediaTypeExceptionCIO(
"Failed to parse multipart: Content-Type should be multipart/* but it is $contentType"
)
} }
val boundaryByteBuffer = parseBoundaryInternal(contentType) val boundaryByteBuffer = parseBoundaryInternal(contentType)
val boundaryBytes = ByteString(boundaryByteBuffer) val boundaryBytes = ByteString(boundaryByteBuffer)
......
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.http.cio.internals
import io.ktor.utils.io.*
import kotlinx.io.IOException
@InternalAPI
public class UnsupportedMediaTypeExceptionCIO(message: String) : IOException(message)
...@@ -699,7 +699,7 @@ final class io.ktor.server.plugins/PayloadTooLargeException : io.ktor.server.plu ...@@ -699,7 +699,7 @@ final class io.ktor.server.plugins/PayloadTooLargeException : io.ktor.server.plu
} }
final class io.ktor.server.plugins/UnsupportedMediaTypeException : io.ktor.server.plugins/ContentTransformationException, kotlinx.coroutines/CopyableThrowable<io.ktor.server.plugins/UnsupportedMediaTypeException> { // io.ktor.server.plugins/UnsupportedMediaTypeException|null[0] final class io.ktor.server.plugins/UnsupportedMediaTypeException : io.ktor.server.plugins/ContentTransformationException, kotlinx.coroutines/CopyableThrowable<io.ktor.server.plugins/UnsupportedMediaTypeException> { // io.ktor.server.plugins/UnsupportedMediaTypeException|null[0]
constructor <init>(io.ktor.http/ContentType) // io.ktor.server.plugins/UnsupportedMediaTypeException.<init>|<init>(io.ktor.http.ContentType){}[0] constructor <init>(io.ktor.http/ContentType?) // io.ktor.server.plugins/UnsupportedMediaTypeException.<init>|<init>(io.ktor.http.ContentType?){}[0]
final fun createCopy(): io.ktor.server.plugins/UnsupportedMediaTypeException // io.ktor.server.plugins/UnsupportedMediaTypeException.createCopy|createCopy(){}[0] final fun createCopy(): io.ktor.server.plugins/UnsupportedMediaTypeException // io.ktor.server.plugins/UnsupportedMediaTypeException.createCopy|createCopy(){}[0]
} }
......
...@@ -85,8 +85,11 @@ public class CannotTransformContentToTypeException( ...@@ -85,8 +85,11 @@ public class CannotTransformContentToTypeException(
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
public class UnsupportedMediaTypeException( public class UnsupportedMediaTypeException(
private val contentType: ContentType private val contentType: ContentType?
) : ContentTransformationException("Content type $contentType is not supported"), ) : ContentTransformationException(
contentType?.let { "Content type $it is not supported" }
?: "Content-Type header is required"
),
CopyableThrowable<UnsupportedMediaTypeException> { CopyableThrowable<UnsupportedMediaTypeException> {
override fun createCopy(): UnsupportedMediaTypeException = UnsupportedMediaTypeException(contentType).also { override fun createCopy(): UnsupportedMediaTypeException = UnsupportedMediaTypeException(contentType).also {
......
...@@ -6,8 +6,10 @@ package io.ktor.server.engine ...@@ -6,8 +6,10 @@ package io.ktor.server.engine
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.cio.* import io.ktor.http.cio.*
import io.ktor.http.cio.internals.*
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.UnsupportedMediaTypeException
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.util.pipeline.* import io.ktor.util.pipeline.*
import io.ktor.utils.io.* import io.ktor.utils.io.*
...@@ -33,16 +35,21 @@ internal actual suspend fun PipelineContext<Any, PipelineCall>.defaultPlatformTr ...@@ -33,16 +35,21 @@ internal actual suspend fun PipelineContext<Any, PipelineCall>.defaultPlatformTr
@OptIn(InternalAPI::class) @OptIn(InternalAPI::class)
internal actual fun PipelineContext<*, PipelineCall>.multiPartData(rc: ByteReadChannel): MultiPartData { internal actual fun PipelineContext<*, PipelineCall>.multiPartData(rc: ByteReadChannel): MultiPartData {
val contentType = call.request.header(HttpHeaders.ContentType) val contentType = call.request.header(HttpHeaders.ContentType)
?: throw IllegalStateException("Content-Type header is required for multipart processing") ?: throw UnsupportedMediaTypeException(null)
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLong() val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLong()
return CIOMultipartDataBase(
coroutineContext + Dispatchers.Unconfined, try {
rc, return CIOMultipartDataBase(
contentType, coroutineContext + Dispatchers.Unconfined,
contentLength, rc,
call.formFieldLimit contentType,
) contentLength,
call.formFieldLimit
)
} catch (_: UnsupportedMediaTypeExceptionCIO) {
throw UnsupportedMediaTypeException(ContentType.parse(contentType))
}
} }
internal actual fun Source.readTextWithCustomCharset(charset: Charset): String = internal actual fun Source.readTextWithCustomCharset(charset: Charset): String =
......
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.tests.server.engine
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
import io.mockk.*
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import kotlin.test.*
class MultiPartDataTest {
private val mockContext = mockk<PipelineContext<*, PipelineCall>>(relaxed = true)
private val mockRequest = mockk<PipelineRequest>(relaxed = true)
private val testScope = TestScope()
@Test
fun givenRequest_whenNoContentTypeHeaderPresent_thenUnsupportedMediaTypeException() {
// Given
every { mockContext.call.request } returns mockRequest
every { mockRequest.header(HttpHeaders.ContentType) } returns null
// When & Then
assertFailsWith<UnsupportedMediaTypeException> {
runBlocking { mockContext.multiPartData(ByteReadChannel("sample data")) }
}
}
@Test
fun givenWrongContentType_whenProcessMultiPart_thenUnsupportedMediaTypeException() {
// Given
val rc = ByteReadChannel("sample data")
val contentType = "test/plain; boundary=test"
val contentLength = "123"
every { mockContext.call.request } returns mockRequest
every { mockContext.call.attributes.getOrNull<Long>(any()) } returns 0L
every { mockRequest.header(HttpHeaders.ContentType) } returns contentType
every { mockRequest.header(HttpHeaders.ContentLength) } returns contentLength
// When & Then
testScope.runTest {
assertFailsWith<UnsupportedMediaTypeException> {
mockContext.multiPartData(rc)
}
}
}
@Test
fun testUnsupportedMediaTypeStatusCode() = testApplication {
routing {
post {
call.receiveMultipart()
call.respond(HttpStatusCode.OK)
}
}
client.post {
accept(ContentType.Text.Plain)
}.apply {
assertEquals(HttpStatusCode.UnsupportedMediaType, status)
}
client.post {}.apply {
assertEquals(HttpStatusCode.UnsupportedMediaType, status)
}
}
}
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать