Skip to content

Commit 92708fa

Browse files
committed
KTOR-8523 Fix for empty part parse exception
1 parent b9b859d commit 92708fa

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

ktor-http/ktor-http-cio/common/src/io/ktor/http/cio/Multipart.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ private suspend fun parsePreambleImpl(
109109
*/
110110
private suspend fun parsePartHeadersImpl(input: ByteReadChannel): HttpHeadersMap {
111111
val builder = CharArrayBuilder()
112-
113112
try {
114113
return parseHeaders(input, builder)
115114
?: throw EOFException("Failed to parse multipart headers: unexpected end of stream")
@@ -215,6 +214,11 @@ private fun CoroutineScope.parseMultipart(
215214
while (!countedInput.isClosedForRead && !countedInput.skipIfFound(PrefixString)) {
216215
countedInput.skipIfFound(CrLf)
217216

217+
// empty contents, no headers
218+
if (countedInput.skipIfFound(firstBoundary)) {
219+
continue
220+
}
221+
218222
val body = ByteChannel()
219223
val headers = CompletableDeferred<HttpHeadersMap>()
220224
val part = MultipartEvent.MultipartPart(headers, body)

ktor-http/ktor-http-cio/jvm/test/io/ktor/tests/http/cio/MultipartTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,57 @@ class MultipartTest {
285285
assertEquals("epilogue", epilogue.body.readText())
286286
}
287287

288+
@OptIn(DelicateCoroutinesApi::class)
289+
@Test
290+
fun testEmptyPart() = runBlocking {
291+
val body = """
292+
POST /send-message.html HTTP/1.1
293+
Host: webmail.example.com
294+
Referer: http://webmail.example.com/send-message.html
295+
User-Agent: BrowserForDummies/4.67b
296+
Content-Type: multipart/form-data; boundary=Asrf456BGe4h
297+
Connection: close
298+
Keep-Alive: 300
299+
300+
preamble
301+
--Asrf456BGe4h
302+
Content-Disposition: form-data; name="DestAddress"
303+
304+
recipient@example.com
305+
--Asrf456BGe4h
306+
--Asrf456BGe4h
307+
Content-Disposition: form-data; name="MessageText"
308+
309+
See attachments...
310+
--Asrf456BGe4h
311+
--Asrf456BGe4h--
312+
epilogue
313+
""".trimIndent()
314+
.lines()
315+
.joinToString("\r\n")
316+
317+
val ch = ByteReadChannel(body.toByteArray())
318+
val request = parseRequest(ch)!!
319+
val mp = parseMultipart(ch, request.headers)
320+
321+
val allEvents = ArrayList<MultipartEvent>()
322+
mp.consumeEach { allEvents.add(it) }
323+
324+
assertEquals(4, allEvents.size)
325+
326+
val preamble = allEvents[0] as MultipartEvent.Preamble
327+
assertEquals("preamble\r\n", preamble.body.readText())
328+
329+
val recipient = allEvents[1] as MultipartEvent.MultipartPart
330+
assertEquals("recipient@example.com", recipient.body.readRemaining().readText())
331+
332+
val text = allEvents[2] as MultipartEvent.MultipartPart
333+
assertEquals("See attachments...", text.body.readRemaining().readText())
334+
335+
val epilogue = allEvents[3] as MultipartEvent.Epilogue
336+
assertEquals("epilogue", epilogue.body.readText())
337+
}
338+
288339
@Test
289340
fun testParseBoundary() {
290341
testBoundary("\r\n--A", "multipart/mixed;boundary=A")

0 commit comments

Comments
 (0)