From 7df930c301b6f6db2ec9aa312093abcb8ac4d5af Mon Sep 17 00:00:00 2001 From: Mario Daniel Ruiz Saavedra Date: Sun, 21 Jun 2026 00:34:18 -0300 Subject: [PATCH] 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(