Skip to content

Commit 77cf4e4

Browse files
osipxdvnikolova
andauthored
KTOR-8367 New SaveBody plugin (#4810)
--------- Co-authored-by: Vik Nikolova <[email protected]>
1 parent 0d3a85a commit 77cf4e4

File tree

11 files changed

+352
-278
lines changed

11 files changed

+352
-278
lines changed

ktor-client/ktor-client-cio/jvm/test/io/ktor/client/engine/cio/ConnectErrorsTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@ class ConnectErrorsTest {
169169
}
170170
}
171171
}
172-
runCatching(prematureDisconnect)
173-
runCatching(prematureDisconnect)
174-
runCatching(respondCorrectly)
172+
prematureDisconnect()
173+
prematureDisconnect()
174+
respondCorrectly()
175175
}
176176

177177
val response = client.get("http://localhost:${serverSocket.localPort}/")

ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClient.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@ import io.ktor.events.*
1414
import io.ktor.util.*
1515
import io.ktor.utils.io.*
1616
import io.ktor.utils.io.core.*
17-
import kotlinx.atomicfu.*
18-
import kotlinx.coroutines.*
19-
import kotlin.coroutines.*
17+
import kotlinx.atomicfu.atomic
18+
import kotlinx.coroutines.CompletableJob
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.Job
21+
import kotlinx.coroutines.cancel
22+
import kotlin.coroutines.CoroutineContext
2023

2124
/**
2225
* A multiplatform asynchronous HTTP client that allows you to make requests, handle responses,
@@ -1372,7 +1375,7 @@ public class HttpClient(
13721375
with(userConfig) {
13731376
config.install(HttpRequestLifecycle)
13741377
config.install(BodyProgress)
1375-
config.install(SaveBodyPlugin)
1378+
config.install(SaveBody)
13761379

13771380
if (useDefaultTransformers) {
13781381
config.install("DefaultTransformers") { defaultTransformers() }

ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DoubleReceivePlugin.kt

Lines changed: 0 additions & 101 deletions
This file was deleted.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
// Preserve the old class name for binary compatibility
6+
@file:JvmName("DoubleReceivePluginKt")
7+
8+
package io.ktor.client.plugins
9+
10+
import io.ktor.client.call.*
11+
import io.ktor.client.plugins.Messages.PLUGIN_DEPRECATED_MESSAGE
12+
import io.ktor.client.plugins.Messages.SAVE_BODY_DISABLED_MESSAGE
13+
import io.ktor.client.plugins.Messages.SAVE_BODY_ENABLED_MESSAGE
14+
import io.ktor.client.plugins.Messages.SKIP_SAVING_BODY_MESSAGE
15+
import io.ktor.client.plugins.api.*
16+
import io.ktor.client.request.*
17+
import io.ktor.client.statement.*
18+
import io.ktor.util.*
19+
import io.ktor.util.logging.*
20+
import io.ktor.utils.io.*
21+
import kotlin.jvm.JvmName
22+
23+
private val SKIP_SAVE_BODY = AttributeKey<Unit>("SkipSaveBody")
24+
private val RESPONSE_BODY_SAVED = AttributeKey<Unit>("ResponseBodySaved")
25+
26+
private val LOGGER by lazy { KtorSimpleLogger("io.ktor.client.plugins.SaveBody") }
27+
28+
/**
29+
* [SaveBody] saves response body in memory, so it can be read multiple times and resources can be freed up immediately.
30+
*
31+
* @see HttpClientCall.save
32+
*/
33+
@OptIn(InternalAPI::class)
34+
internal val SaveBody: ClientPlugin<Unit> = createClientPlugin("SaveBody") {
35+
client.receivePipeline.intercept(HttpReceivePipeline.Before) { response ->
36+
val call = response.call
37+
val attributes = call.attributes
38+
if (attributes.contains(SKIP_SAVE_BODY)) {
39+
LOGGER.trace { "Skipping body saving for ${call.request.url}" }
40+
return@intercept
41+
}
42+
43+
val savedResponse = try {
44+
LOGGER.trace { "Saving body for ${call.request.url}" }
45+
call.save().response
46+
} finally {
47+
runCatching { response.rawContent.cancel() }
48+
.onFailure { LOGGER.debug("Failed to cancel response body", it) }
49+
}
50+
51+
attributes.put(RESPONSE_BODY_SAVED, Unit)
52+
proceedWith(savedResponse)
53+
}
54+
}
55+
56+
internal fun HttpRequestBuilder.skipSaveBody() {
57+
attributes.put(SKIP_SAVE_BODY, Unit)
58+
}
59+
60+
/**
61+
* Configuration for [SaveBodyPlugin]
62+
*
63+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.SaveBodyPluginConfig)
64+
*/
65+
@Deprecated(PLUGIN_DEPRECATED_MESSAGE)
66+
public class SaveBodyPluginConfig {
67+
/**
68+
* Disables the plugin for all requests.
69+
* Note that the body of streaming responses is not saved.
70+
*
71+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.SaveBodyPluginConfig.disabled)
72+
*/
73+
@Deprecated(SAVE_BODY_DISABLED_MESSAGE)
74+
public var disabled: Boolean = false
75+
}
76+
77+
/**
78+
* [SaveBodyPlugin] saving the whole body in memory, so it can be received multiple times.
79+
*
80+
* It may be useful to prevent saving body in case of big size or streaming. To do so use [HttpRequestBuilder.skipSavingBody]:
81+
* ```kotlin
82+
* client.get("http://myurl.com") {
83+
* skipSavingBody()
84+
* }
85+
* ```
86+
*
87+
* The plugin is installed by default, if you need to disable it use:
88+
* ```kotlin
89+
* val client = HttpClient {
90+
* install(SaveBodyPlugin) {
91+
* disabled = true
92+
* }
93+
* }
94+
* ```
95+
*
96+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.SaveBodyPlugin)
97+
*/
98+
@Suppress("DEPRECATION")
99+
@Deprecated(PLUGIN_DEPRECATED_MESSAGE)
100+
public val SaveBodyPlugin: ClientPlugin<SaveBodyPluginConfig> = createClientPlugin(
101+
"DoubleReceivePlugin",
102+
::SaveBodyPluginConfig
103+
) {
104+
if (pluginConfig.disabled) {
105+
LOGGER.warn(SAVE_BODY_DISABLED_MESSAGE)
106+
} else {
107+
LOGGER.warn(SAVE_BODY_ENABLED_MESSAGE)
108+
}
109+
}
110+
111+
/**
112+
* Returns `true` if response body is saved and can be read multiple times.
113+
* By default, all non-streaming responses are saved.
114+
*
115+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.isSaved)
116+
*/
117+
public val HttpResponse.isSaved: Boolean
118+
get() = call.attributes.contains(RESPONSE_BODY_SAVED)
119+
120+
/**
121+
* Prevent saving response body in memory for the specific request.
122+
*
123+
* To disable the plugin for all requests use [SaveBodyPluginConfig.disabled] property:
124+
* ```kotlin
125+
* val client = HttpClient {
126+
* install(SaveBodyPlugin) {
127+
* disabled = true
128+
* }
129+
* }
130+
* ```
131+
*
132+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.plugins.skipSavingBody)
133+
*/
134+
@Deprecated(SKIP_SAVING_BODY_MESSAGE)
135+
@Suppress("UnusedReceiverParameter")
136+
public fun HttpRequestBuilder.skipSavingBody() {
137+
LOGGER.warn(SKIP_SAVING_BODY_MESSAGE)
138+
}
139+
140+
@Suppress("ConstPropertyName")
141+
private object Messages {
142+
private const val `use streaming syntax` =
143+
"Use client.prepareRequest(...).execute { ... } syntax to prevent saving the body in memory."
144+
private const val `api will be removed` =
145+
"This API is deprecated and will be removed in Ktor 4.0.0"
146+
private const val `share use case` =
147+
"If you were relying on this functionality, share your use case by commenting on this issue: " +
148+
"https://youtrack.jetbrains.com/issue/KTOR-8367/"
149+
150+
const val SAVE_BODY_ENABLED_MESSAGE =
151+
"The SaveBodyPlugin plugin is deprecated and can be safely removed. " +
152+
"Request bodies are now saved in memory by default for all non-streaming responses."
153+
154+
const val SAVE_BODY_DISABLED_MESSAGE =
155+
"It is no longer possible to disable body saving for all requests. " +
156+
`use streaming syntax` + "\n\n" +
157+
`api will be removed` + "\n" +
158+
`share use case`
159+
160+
const val PLUGIN_DEPRECATED_MESSAGE =
161+
"This plugin is no longer needed.\n" +
162+
`api will be removed`
163+
164+
const val SKIP_SAVING_BODY_MESSAGE =
165+
"Skipping of body saving for a specific request is no longer allowed.\n" +
166+
`use streaming syntax` + "\n\n" +
167+
`api will be removed` + "\n" +
168+
`share use case`
169+
}

0 commit comments

Comments
 (0)