Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KTOR-7470 receiveMultipart throw UnsupportedMediaTypeException #4339

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Copyright 2014-2020 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

# config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.io.*
import kotlinx.io.IOException
import kotlinx.io.bytestring.*
import java.io.EOFException
import java.nio.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,11 @@ public class CannotTransformContentToTypeException(
*/
@OptIn(ExperimentalCoroutinesApi::class)
public class UnsupportedMediaTypeException(
private val contentType: ContentType
) : ContentTransformationException("Content type $contentType is not supported"),
CopyableThrowable<UnsupportedMediaTypeException> {
private val contentType: ContentType?
) : ContentTransformationException(
contentType?.let { "Content type $it is not supported" }
?: "Content-Type header is required for multipart processing"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Content-Type header may be required not only for multipart requests, we can remove this part

), CopyableThrowable<UnsupportedMediaTypeException> {

override fun createCopy(): UnsupportedMediaTypeException = UnsupportedMediaTypeException(contentType).also {
it.initCauseBridge(this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.ktor.http.*
import io.ktor.http.cio.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.plugins.UnsupportedMediaTypeException
import io.ktor.server.request.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
Expand All @@ -17,6 +18,7 @@ import io.ktor.utils.io.streams.*
import kotlinx.coroutines.*
import kotlinx.io.*
import java.io.*
import java.io.IOException

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also one more place, where exception is thrown - CIO:
1)

val contentType = headers["Content-Type"] ?: throw IOException("Failed to parse multipart: no Content-Type header")

2)
throw IOException("Failed to parse multipart: Content-Type should be multipart/* but it is $contentType")

Can we please fix it too?


val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLong()
return CIOMultipartDataBase(
coroutineContext + Dispatchers.Unconfined,
rc,
contentType,
contentLength,
call.formFieldLimit
)

try {
return CIOMultipartDataBase(
coroutineContext + Dispatchers.Unconfined,
rc,
contentType,
contentLength,
call.formFieldLimit
)
} catch (_: IOException) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need try-catch here? It's a constructor method

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not have an access to UnsupportedMediaTypeException in CIO module

I had to catch an exception on the server and rethrow it here, it is the latest server code before CIO module

This should answer this question

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Can we add a check that the exception message contains "Content-Type " because another IOException also thrown

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added this check, but maybe there could be a way to add UnsupportedMediaTypeException to CIO module or create separate Exception for the Unsupported Media errors in CIO?

Not sure if it is a common practice to define catch logic based on exception's message

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, adding UnsupportedMediaTypeException to CIO module is a breaking change so can we please create a new Exception in CIO module UnsupportedMediaTypeExceptionCIO and catch it in DefaultTransformJvm?

throw UnsupportedMediaTypeException(ContentType.parse(contentType))
}
}

internal actual fun Source.readTextWithCustomCharset(charset: Charset): String =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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.http.*
import io.ktor.server.application.*
import io.ktor.server.engine.multiPartData
import io.ktor.server.plugins.UnsupportedMediaTypeException
import io.ktor.server.request.*
import io.ktor.util.pipeline.*
import io.ktor.utils.io.*
import io.mockk.*
import kotlinx.coroutines.*
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
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() {
// Setup
every { mockContext.call.request } returns mockRequest
every { mockRequest.header(HttpHeaders.ContentType) } returns null

// Act & Assert
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)
}
}
}
}