Skip to content

Don't close the multipart sink when a part closes its stream - #9659

Open
iVamsi wants to merge 6 commits into
lysine-dev:mainfrom
iVamsi:fix/gzip-multipart-closed-7692
Open

Don't close the multipart sink when a part closes its stream#9659
iVamsi wants to merge 6 commits into
lysine-dev:mainfrom
iVamsi:fix/gzip-multipart-closed-7692

Conversation

@iVamsi

@iVamsi iVamsi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

A request body closing its sink was also closing the stream that OkHttp was still writing to. In a multipart request, that meant the next part (or even the bytes after the first part) failed with IllegalStateException: closed.

The part still needs to finish its stream. This change lets it do that without closing the stream MultipartBody was handed.

Fixes #7692.

@JakeWharton

Copy link
Copy Markdown
Collaborator

This seems like it fixes a very specific variant of this bug, but not the whole class of bug.

@iVamsi iVamsi changed the title Don't close the stream when gzipping a request body Don't close the multipart sink when a part closes its stream Aug 13, 2026
@iVamsi

iVamsi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@JakeWharton Yeah, gzip was just one body that closed the sink. Moved the fix to MultipartBody.

@JakeWharton JakeWharton left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just some small tweaks

Comment on lines +165 to +170
object : ForwardingSink(sink) {
override fun close() {
// GzipSink.close() writes the gzip footer, then closes whatever it wraps.
// Swallow that close so we can write the trailing CRLF and later parts.
}
}.buffer().use(body::writeTo)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
object : ForwardingSink(sink) {
override fun close() {
// GzipSink.close() writes the gzip footer, then closes whatever it wraps.
// Swallow that close so we can write the trailing CRLF and later parts.
}
}.buffer().use(body::writeTo)
class NoCloseSink(sink: Sink) : ForwardingSink(sink) {
override fun close() {}
}
body.writeTo(NoCloseSink(sink).buffer())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The use is pointless here, because it guarantees a close() call which we're already going out of our way to suppress.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

.buffer() adds a RealBufferedSink on top of NoCloseSink. .use closes it so the last segment is flushed. NoCloseSink.close() is still a no-op.

Without .use, 9 of 14 MultipartBodyTest tests failed.

open fun contentLength(): Long = -1L

/** Writes the content of this request to [sink]. */
/** Writes the content of this request to [sink]. This must not close [sink]. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/** Writes the content of this request to [sink]. This must not close [sink]. */
/** Writes the content of this request to [sink]. This should not close [sink]. */

Either we guarantee this by crashing, or we relax the wording precisely because we are handling close appropriately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Comment on lines +293 to +298
assertThat(body.boundary).isEqualTo("123")
assertThat(body.type).isEqualTo(MultipartBody.MIXED)
assertThat(body.contentType().toString())
.isEqualTo("multipart/mixed; boundary=123")
assertThat(body.parts.size).isEqualTo(2)
assertThat(body.contentLength()).isEqualTo(-1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
assertThat(body.boundary).isEqualTo("123")
assertThat(body.type).isEqualTo(MultipartBody.MIXED)
assertThat(body.contentType().toString())
.isEqualTo("multipart/mixed; boundary=123")
assertThat(body.parts.size).isEqualTo(2)
assertThat(body.contentLength()).isEqualTo(-1)

This is other test problems

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

Comment on lines +316 to +321
assertThat(body.boundary).isEqualTo("123")
assertThat(body.type).isEqualTo(MultipartBody.MIXED)
assertThat(body.contentType().toString())
.isEqualTo("multipart/mixed; boundary=123")
assertThat(body.parts.size).isEqualTo(2)
assertThat(body.contentLength()).isEqualTo(-1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
assertThat(body.boundary).isEqualTo("123")
assertThat(body.type).isEqualTo(MultipartBody.MIXED)
assertThat(body.contentType().toString())
.isEqualTo("multipart/mixed; boundary=123")
assertThat(body.parts.size).isEqualTo(2)
assertThat(body.contentLength()).isEqualTo(-1)

Other test problems

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed

Comment on lines +330 to +345
MultipartReader(boundary = "123", source = buffer).use { reader ->
val part1 = reader.nextPart()!!
val part1Body =
GzipSource(part1.body).use {
it.buffer().readUtf8()
}
assertThat(part1Body).isEqualTo("part1")

val part2 = reader.nextPart()!!
val part2Body =
GzipSource(part2.body).use {
it.buffer().readUtf8()
}
assertThat(part2Body).isEqualTo("part2")
assertThat(reader.nextPart()).isNull()
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would rather see this all replaced with a readUtf8() like the above test. We don't care about parsing, we care that the entire request was written.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

okhttp3.RequestBody.Companion#gzip throws java.lang.IllegalStateException: closed if used in MultipartBody

2 participants