From 24c7f7d5c529bcab203fc6fe100e76cd43ef5ae9 Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Sun, 21 Jun 2026 00:34:18 -0300 Subject: [PATCH 1/3] Add RFC 1008 (QUERY Method) support --- .../okhttp3/internal/http/HttpMethod.kt | 4 +- .../http/RetryAndFollowUpInterceptor.kt | 9 +- okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt | 90 +++++++++++++++++++ 3 files changed, 98 insertions(+), 5 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt index 77d2d9da5a73..910ef22c08b2 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt @@ -43,9 +43,9 @@ object HttpMethod { @JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs. fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD") - fun redirectsWithBody(method: String): Boolean = method == "PROPFIND" + fun redirectsWithBody(method: String): Boolean = method == "PROPFIND" || method == "QUERY" - fun redirectsToGet(method: String): Boolean = method != "PROPFIND" + fun redirectsToGet(method: String): Boolean = method != "PROPFIND" && method != "QUERY" fun isCacheable(requestMethod: String): Boolean = requestMethod == "GET" || requestMethod == "QUERY" } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt index 458c3eaad396..79d6cf8933c7 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt @@ -313,11 +313,14 @@ class RetryAndFollowUpInterceptor : Interceptor { val requestBuilder = userResponse.request.newBuilder() if (HttpMethod.permitsRequestBody(method)) { val responseCode = userResponse.code + val isQueryAndSeeOther = method == "QUERY" && responseCode == HTTP_SEE_OTHER val maintainBody = - HttpMethod.redirectsWithBody(method) || + (HttpMethod.redirectsWithBody(method) || responseCode == HTTP_PERM_REDIRECT || - responseCode == HTTP_TEMP_REDIRECT - if (HttpMethod.redirectsToGet(method) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT) { + responseCode == HTTP_TEMP_REDIRECT) && !isQueryAndSeeOther + if ((HttpMethod.redirectsToGet(method) || isQueryAndSeeOther) && + responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT + ) { requestBuilder.method("GET", null) } else { val requestBody = if (maintainBody) userResponse.request.body else null diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt index 35c86efecd63..831239ddee03 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/CallTest.kt @@ -2490,6 +2490,96 @@ open class CallTest { assertThat(page2.body?.utf8()).isEqualTo("Request Body") } + @Test + fun queryRedirectsToQueryAndMaintainsRequestBodyOnMovedTemp() { + server.enqueue( + MockResponse( + code = HttpURLConnection.HTTP_MOVED_TEMP, + headers = headersOf("Location", "/page2"), + body = "This page has moved!", + ), + ) + server.enqueue(MockResponse(body = "Page 2")) + + val response = + client + .newCall( + Request + .Builder() + .url(server.url("/page1")) + .query("Query Body".toRequestBody("text/plain".toMediaType())) + .build(), + ).execute() + + assertThat(response.body.string()).isEqualTo("Page 2") + val page1 = server.takeRequest() + assertThat(page1.requestLine).isEqualTo("QUERY /page1 HTTP/1.1") + assertThat(page1.body?.utf8()).isEqualTo("Query Body") + val page2 = server.takeRequest() + assertThat(page2.requestLine).isEqualTo("QUERY /page2 HTTP/1.1") + assertThat(page2.body?.utf8()).isEqualTo("Query Body") + } + + @Test + fun queryRedirectsToQueryAndMaintainsRequestBodyOnMovedPerm() { + server.enqueue( + MockResponse( + code = HttpURLConnection.HTTP_MOVED_PERM, + headers = headersOf("Location", "/page2"), + body = "This page has moved!", + ), + ) + server.enqueue(MockResponse(body = "Page 2")) + + val response = + client + .newCall( + Request + .Builder() + .url(server.url("/page1")) + .query("Query Body".toRequestBody("text/plain".toMediaType())) + .build(), + ).execute() + + assertThat(response.body.string()).isEqualTo("Page 2") + val page1 = server.takeRequest() + assertThat(page1.requestLine).isEqualTo("QUERY /page1 HTTP/1.1") + assertThat(page1.body?.utf8()).isEqualTo("Query Body") + val page2 = server.takeRequest() + assertThat(page2.requestLine).isEqualTo("QUERY /page2 HTTP/1.1") + assertThat(page2.body?.utf8()).isEqualTo("Query Body") + } + + @Test + fun queryRedirectsToGetAndDropsRequestBodyOnSeeOther() { + server.enqueue( + MockResponse( + code = HttpURLConnection.HTTP_SEE_OTHER, + headers = headersOf("Location", "/page2"), + body = "See other page!", + ), + ) + server.enqueue(MockResponse(body = "Page 2")) + + val response = + client + .newCall( + Request + .Builder() + .url(server.url("/page1")) + .query("Query Body".toRequestBody("text/plain".toMediaType())) + .build(), + ).execute() + + assertThat(response.body.string()).isEqualTo("Page 2") + val page1 = server.takeRequest() + assertThat(page1.requestLine).isEqualTo("QUERY /page1 HTTP/1.1") + assertThat(page1.body?.utf8()).isEqualTo("Query Body") + val page2 = server.takeRequest() + assertThat(page2.requestLine).isEqualTo("GET /page2 HTTP/1.1") + assertThat(page2.body).isNull() + } + @Test fun responseCookies() { server.enqueue( From 5f15489d1e7152773c8159414f620f807a19adc0 Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Wed, 24 Jun 2026 02:04:24 -0300 Subject: [PATCH 2/3] Spotless apply --- .../okhttp3/internal/http/RetryAndFollowUpInterceptor.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt index 79d6cf8933c7..faa662714d44 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt @@ -315,9 +315,11 @@ class RetryAndFollowUpInterceptor : Interceptor { val responseCode = userResponse.code val isQueryAndSeeOther = method == "QUERY" && responseCode == HTTP_SEE_OTHER val maintainBody = - (HttpMethod.redirectsWithBody(method) || - responseCode == HTTP_PERM_REDIRECT || - responseCode == HTTP_TEMP_REDIRECT) && !isQueryAndSeeOther + ( + HttpMethod.redirectsWithBody(method) || + responseCode == HTTP_PERM_REDIRECT || + responseCode == HTTP_TEMP_REDIRECT + ) && !isQueryAndSeeOther if ((HttpMethod.redirectsToGet(method) || isQueryAndSeeOther) && responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT ) { From a64ff63ed83e44c1e385cdc9e513f75d82df90ad Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Sun, 28 Jun 2026 13:41:48 -0300 Subject: [PATCH 3/3] Add responseCode to redirectsToGet --- .../kotlin/okhttp3/internal/http/HttpMethod.kt | 12 +++++++++--- .../http/RetryAndFollowUpInterceptor.kt | 18 +++--------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt index 910ef22c08b2..21a612171725 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/HttpMethod.kt @@ -15,6 +15,7 @@ */ package okhttp3.internal.http +import java.net.HttpURLConnection.HTTP_SEE_OTHER import kotlin.jvm.JvmStatic object HttpMethod { @@ -43,9 +44,14 @@ object HttpMethod { @JvmStatic // Despite being 'internal', this method is called by popular 3rd party SDKs. fun permitsRequestBody(method: String): Boolean = !(method == "GET" || method == "HEAD") - fun redirectsWithBody(method: String): Boolean = method == "PROPFIND" || method == "QUERY" - - fun redirectsToGet(method: String): Boolean = method != "PROPFIND" && method != "QUERY" + fun redirectsToGet( + method: String, + responseCode: Int, + ): Boolean { + if (responseCode == HTTP_SEE_OTHER) return method != "PROPFIND" + if (responseCode == HTTP_TEMP_REDIRECT || responseCode == HTTP_PERM_REDIRECT) return false + return method != "PROPFIND" && method != "QUERY" + } fun isCacheable(requestMethod: String): Boolean = requestMethod == "GET" || requestMethod == "QUERY" } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt index faa662714d44..ca03741b6ca4 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/http/RetryAndFollowUpInterceptor.kt @@ -313,25 +313,13 @@ class RetryAndFollowUpInterceptor : Interceptor { val requestBuilder = userResponse.request.newBuilder() if (HttpMethod.permitsRequestBody(method)) { val responseCode = userResponse.code - val isQueryAndSeeOther = method == "QUERY" && responseCode == HTTP_SEE_OTHER - val maintainBody = - ( - HttpMethod.redirectsWithBody(method) || - responseCode == HTTP_PERM_REDIRECT || - responseCode == HTTP_TEMP_REDIRECT - ) && !isQueryAndSeeOther - if ((HttpMethod.redirectsToGet(method) || isQueryAndSeeOther) && - responseCode != HTTP_PERM_REDIRECT && responseCode != HTTP_TEMP_REDIRECT - ) { + if (HttpMethod.redirectsToGet(method, responseCode)) { requestBuilder.method("GET", null) - } else { - val requestBody = if (maintainBody) userResponse.request.body else null - requestBuilder.method(method, requestBody) - } - if (!maintainBody) { requestBuilder.removeHeader("Transfer-Encoding") requestBuilder.removeHeader("Content-Length") requestBuilder.removeHeader("Content-Type") + } else { + requestBuilder.method(method, userResponse.request.body) } }