From 6a22d2b35f8c1cecdfd86ce86afaff1f0497c905 Mon Sep 17 00:00:00 2001 From: Amir Golmoradi Date: Thu, 22 Oct 2026 02:24:11 +0330 Subject: [PATCH 1/2] feat(okhttp): expose ForwardingRequestBody in the main module --- okhttp/api/android/okhttp.api | 11 +++ okhttp/api/jvm/okhttp.api | 11 +++ .../kotlin/okhttp3/ForwardingRequestBody.kt | 3 + .../okhttp3/ForwardingRequestBodyTest.kt | 96 +++++++++++++++++++ 4 files changed, 121 insertions(+) rename {okhttp-testing-support/src/main => okhttp/src/commonJvmAndroid}/kotlin/okhttp3/ForwardingRequestBody.kt (90%) create mode 100644 okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 26f1219816b8..446dc7fab98d 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -624,6 +624,17 @@ public final class okhttp3/FormBody$Builder { public final class okhttp3/FormBody$Companion { } +public class okhttp3/ForwardingRequestBody : okhttp3/RequestBody { + public fun (Lokhttp3/RequestBody;)V + public fun contentLength ()J + public fun contentType ()Lokhttp3/MediaType; + public final fun delegate ()Lokhttp3/RequestBody; + public fun isDuplex ()Z + public fun isOneShot ()Z + public fun toString ()Ljava/lang/String; + public fun writeTo (Lokio/BufferedSink;)V +} + public final class okhttp3/Gzip : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/Gzip; public fun decompress (Lokio/BufferedSource;)Lokio/Source; diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index aae4cbd894b5..e8a11f2ba41c 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -624,6 +624,17 @@ public final class okhttp3/FormBody$Builder { public final class okhttp3/FormBody$Companion { } +public class okhttp3/ForwardingRequestBody : okhttp3/RequestBody { + public fun (Lokhttp3/RequestBody;)V + public fun contentLength ()J + public fun contentType ()Lokhttp3/MediaType; + public final fun delegate ()Lokhttp3/RequestBody; + public fun isDuplex ()Z + public fun isOneShot ()Z + public fun toString ()Ljava/lang/String; + public fun writeTo (Lokio/BufferedSink;)V +} + public final class okhttp3/Gzip : okhttp3/CompressionInterceptor$DecompressionAlgorithm { public static final field INSTANCE Lokhttp3/Gzip; public fun decompress (Lokio/BufferedSource;)Lokio/Source; diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/ForwardingRequestBody.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt similarity index 90% rename from okhttp-testing-support/src/main/kotlin/okhttp3/ForwardingRequestBody.kt rename to okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt index 164f709c5c59..9174b801bd31 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/ForwardingRequestBody.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt @@ -35,8 +35,11 @@ open class ForwardingRequestBody( delegate.writeTo(sink) } + // Duplex allows the request body to be written while the response body is read. override fun isDuplex(): Boolean = delegate.isDuplex() + override fun isOneShot(): Boolean = delegate.isOneShot() + override fun toString(): String = javaClass.simpleName + "(" + delegate.toString() + ")" init { diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt new file mode 100644 index 000000000000..ff24487c64d7 --- /dev/null +++ b/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt @@ -0,0 +1,96 @@ +package okhttp3 + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isTrue +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import okio.Buffer +import okio.BufferedSink +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource +import org.junit.jupiter.params.provider.NullSource +import org.junit.jupiter.params.provider.ValueSource + +open class ForwardingRequestBodyTest { + @Test + fun forwardsIsOneShot() { + val delegate = + object : RequestBody() { + override fun contentType(): MediaType? = null + + override fun writeTo(sink: BufferedSink) {} + + override fun isOneShot(): Boolean = true + } + + val forwardingBody = ForwardingRequestBody(delegate) + + assertThat(forwardingBody.isOneShot()).isTrue() + } + + /** + * Verifies that overriding the content type preserves the delegate's content, + * content length, and one-shot and duplex flags. + */ + @ParameterizedTest + @CsvSource( + "false, false", + "false, true", + "true, false", + "true, true", + ) + fun overridingContentTypePreservesBodyBehavior( + oneShot: Boolean, + duplex: Boolean, + ) { + val originalType = "text/plain".toMediaType() + val overriddenType = "application/octet-stream".toMediaType() + + val delegate = + object : RequestBody() { + override fun contentType(): MediaType = originalType + + override fun contentLength(): Long = 5L + + override fun writeTo(sink: BufferedSink) { + sink.writeUtf8("hello") + } + + override fun isOneShot(): Boolean = oneShot + + override fun isDuplex(): Boolean = duplex + } + + val forwardingBody = + object : ForwardingRequestBody(delegate) { + override fun contentType(): MediaType = overriddenType + } + + assertThat(forwardingBody.contentType()).isEqualTo(overriddenType) + assertThat(delegate.contentType()).isEqualTo(originalType) + assertThat(forwardingBody.contentLength()).isEqualTo(5L) + assertThat(forwardingBody.isOneShot()).isEqualTo(oneShot) + assertThat(forwardingBody.isDuplex()).isEqualTo(duplex) + + val sink = Buffer() + forwardingBody.writeTo(sink) + assertThat(sink.readUtf8()).isEqualTo("hello") + } + + /** + * Verifies that the delegate's content type is forwarded, including null + * when the delegate does not specify one. + */ + @ParameterizedTest + @NullSource + @ValueSource(strings = ["text/plain"]) + fun forwardsContentType(contentType: String?) { + val mediaType = contentType?.toMediaType() + val delegate = byteArrayOf(1, 2, 3).toRequestBody(mediaType) + val forwardingBody = ForwardingRequestBody(delegate) + + assertThat(forwardingBody.contentType()).isEqualTo(mediaType) + } +} From 91886de047e4f59b091d10005287d4102620c28d Mon Sep 17 00:00:00 2001 From: Amir Golmoradi Date: Tue, 27 Oct 2026 19:16:18 +0330 Subject: [PATCH 2/2] fix: require a non-null forwarding body delegate and remove redundant test --- .../okhttp3/UppercaseRequestInterceptor.kt | 8 +++----- .../kotlin/okhttp3/ForwardingRequestBody.kt | 10 +--------- .../okhttp3/ForwardingRequestBodyTest.kt | 18 ------------------ 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseRequestInterceptor.kt b/okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseRequestInterceptor.kt index e9028f13e94a..362b6c98a1a0 100644 --- a/okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseRequestInterceptor.kt +++ b/okhttp-testing-support/src/main/kotlin/okhttp3/UppercaseRequestInterceptor.kt @@ -30,17 +30,15 @@ class UppercaseRequestInterceptor : Interceptor { /** Returns a request that transforms `request` to be all uppercase. */ private fun uppercaseRequest(request: Request): Request { + val body = request.body ?: return request val uppercaseBody: RequestBody = - object : ForwardingRequestBody(request.body) { + object : ForwardingRequestBody(delegate = body) { @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { delegate().writeTo(uppercaseSink(sink).buffer()) } } - return request - .newBuilder() - .method(request.method, uppercaseBody) - .build() + return request.newBuilder().method(request.method, uppercaseBody).build() } private fun uppercaseSink(sink: Sink): Sink = diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt index 9174b801bd31..764e9749e1cf 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ForwardingRequestBody.kt @@ -19,10 +19,8 @@ import java.io.IOException import okio.BufferedSink open class ForwardingRequestBody( - delegate: RequestBody?, + private val delegate: RequestBody, ) : RequestBody() { - private val delegate: RequestBody - fun delegate(): RequestBody = delegate override fun contentType(): MediaType? = delegate.contentType() @@ -35,15 +33,9 @@ open class ForwardingRequestBody( delegate.writeTo(sink) } - // Duplex allows the request body to be written while the response body is read. override fun isDuplex(): Boolean = delegate.isDuplex() override fun isOneShot(): Boolean = delegate.isOneShot() override fun toString(): String = javaClass.simpleName + "(" + delegate.toString() + ")" - - init { - requireNotNull(delegate) { "delegate == null" } - this.delegate = delegate - } } diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt index ff24487c64d7..62f309253207 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/ForwardingRequestBodyTest.kt @@ -2,34 +2,16 @@ package okhttp3 import assertk.assertThat import assertk.assertions.isEqualTo -import assertk.assertions.isTrue import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import okio.Buffer import okio.BufferedSink -import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.junit.jupiter.params.provider.NullSource import org.junit.jupiter.params.provider.ValueSource open class ForwardingRequestBodyTest { - @Test - fun forwardsIsOneShot() { - val delegate = - object : RequestBody() { - override fun contentType(): MediaType? = null - - override fun writeTo(sink: BufferedSink) {} - - override fun isOneShot(): Boolean = true - } - - val forwardingBody = ForwardingRequestBody(delegate) - - assertThat(forwardingBody.isOneShot()).isTrue() - } - /** * Verifies that overriding the content type preserves the delegate's content, * content length, and one-shot and duplex flags.