Skip to content

Commit ca87d8c

Browse files
committed
chore: simplify http/response memoization, docs, and exception constructors
Three self-contained cleanups in the http/response subsystem, none of which changes runtime behavior: - ParsedResponse: replace the bespoke private Outcome sealed class with a memoized kotlin.Result. runCatching/getOrThrow preserve the exact Throwable-catching and memoize-before-throw semantics, and a nullable Result<T>? keeps the deliberate null-success vs. unparsed distinction. - ResponseBody: rewrite the class KDoc around the real source(): BufferedSource contract (it previously described byteStream/bytes/string methods the type never declared) and drop the now-unused java.io.InputStream import. - HttpException: add one protected constructor that unpacks status/headers/body from a Response, and collapse the 18 subclasses' identical six-argument delegation blocks to a single-line delegation. Subclass public constructor surfaces are unchanged; the new protected constructor is reflected in the committed API snapshot.
1 parent 21421e0 commit ca87d8c

5 files changed

Lines changed: 55 additions & 190 deletions

File tree

sdk-core/api/sdk-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,6 +1435,7 @@ public class org/dexpace/sdk/core/http/response/exception/GoneException : org/de
14351435
public abstract class org/dexpace/sdk/core/http/response/exception/HttpException : java/lang/RuntimeException, org/dexpace/sdk/core/http/response/exception/Retryable {
14361436
public static final field Companion Lorg/dexpace/sdk/core/http/response/exception/HttpException$Companion;
14371437
public static final field DEFAULT_SNAPSHOT_BYTES I
1438+
protected fun <init> (Lorg/dexpace/sdk/core/http/response/Response;Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
14381439
public fun <init> (Lorg/dexpace/sdk/core/http/response/Status;Lorg/dexpace/sdk/core/http/common/Headers;Lorg/dexpace/sdk/core/http/response/ResponseBody;)V
14391440
public fun <init> (Lorg/dexpace/sdk/core/http/response/Status;Lorg/dexpace/sdk/core/http/common/Headers;Lorg/dexpace/sdk/core/http/response/ResponseBody;Ljava/lang/String;)V
14401441
public fun <init> (Lorg/dexpace/sdk/core/http/response/Status;Lorg/dexpace/sdk/core/http/common/Headers;Lorg/dexpace/sdk/core/http/response/ResponseBody;Ljava/lang/String;Ljava/lang/Throwable;)V

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/ParsedResponse.kt

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ public class ParsedResponse<out T> internal constructor(
5252
) : Closeable {
5353
private val lock = ReentrantLock()
5454

55-
// Holds the memoized outcome once the handler has run. A non-null holder means "parsed"
56-
// (success or failure); the wrapped value distinguishes the two. A holder (rather than a
55+
// Holds the memoized outcome once the handler has run. A non-null Result means "parsed"
56+
// (success or failure); the wrapped value distinguishes the two. A boxed Result (rather than a
5757
// bare value) lets a legitimately-null success memoize without being mistaken for "unparsed".
5858
@Volatile
59-
private var outcome: Outcome<T>? = null
59+
private var outcome: Result<T>? = null
6060

6161
/** The request that produced [raw]. Does not parse. */
6262
public val request: Request get() = raw.request
@@ -93,23 +93,14 @@ public class ParsedResponse<out T> internal constructor(
9393
*/
9494
@Throws(IOException::class)
9595
public fun value(): T {
96-
outcome?.let { return it.get() }
96+
outcome?.let { return it.getOrThrow() }
9797
return lock.withLock {
98-
outcome?.let { return it.get() }
99-
// Memoize the handler's outcome — success or failure — so neither re-runs the handler
100-
// nor re-reads the (now consumed) body on a subsequent call.
101-
val resolved: Outcome<T> =
102-
try {
103-
Outcome.Success(handler.handle(raw))
104-
} catch (t: Throwable) {
105-
// Catch Throwable, not Exception, on purpose: once the handler has touched the
106-
// single-use body, re-running it would read an already-consumed stream. Even an
107-
// Error (e.g. an OOM mid-parse) is memoized so a later call re-throws it rather
108-
// than re-reading the body and masking the original failure.
109-
Outcome.Failure(t)
110-
}
111-
outcome = resolved
112-
resolved.get()
98+
outcome?.let { return it.getOrThrow() }
99+
// Memoize the outcome (success or failure) so a later call neither re-runs the handler
100+
// nor re-reads the now-consumed body. `runCatching` catches `Throwable`, not just
101+
// `Exception`: re-running a handler that already drained the single-use body would read
102+
// a consumed stream, so even an `Error` (e.g. OOM mid-parse) is memoized and re-thrown.
103+
runCatching { handler.handle(raw) }.also { outcome = it }.getOrThrow()
113104
}
114105
}
115106

@@ -124,18 +115,6 @@ public class ParsedResponse<out T> internal constructor(
124115
raw.close()
125116
}
126117

127-
private sealed class Outcome<out T> {
128-
abstract fun get(): T
129-
130-
class Success<out T>(private val value: T) : Outcome<T>() {
131-
override fun get(): T = value
132-
}
133-
134-
class Failure(private val error: Throwable) : Outcome<Nothing>() {
135-
override fun get(): Nothing = throw error
136-
}
137-
}
138-
139118
public companion object {
140119
/**
141120
* Creates a [ParsedResponse] that parses [response] with [handler] on first access.

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/ResponseBody.kt

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,32 @@ import org.dexpace.sdk.core.http.common.MediaType
1111
import org.dexpace.sdk.core.io.BufferedSource
1212
import java.io.Closeable
1313
import java.io.IOException
14-
import java.io.InputStream
1514

1615
/**
1716
* Represents the body of an HTTP response.
1817
*
19-
* A `ResponseBody` provides access to the raw bytes of an HTTP response through [byteStream],
20-
* with convenience methods [bytes] and [string] for common consumption patterns. The body
21-
* **must be closed** after use to release the underlying connection — prefer Kotlin's `use {}`
22-
* or Java's try-with-resources.
18+
* A `ResponseBody` exposes the raw bytes of an HTTP response through a single [source] accessor
19+
* returning a [BufferedSource]. The body **must be closed** after use to release the underlying
20+
* connection — prefer Kotlin's `use {}` or Java's try-with-resources, and close it explicitly even
21+
* when the body is skipped without reading.
2322
*
24-
* This class uses only `java.io` APIs with no external dependencies, making it compatible
25-
* with JDK 8+ and safe to use from platform threads, virtual threads, Kotlin coroutines,
26-
* and reactive schedulers. The underlying [InputStream] performs blocking I/O; callers in
23+
* This class depends only on the SDK's [BufferedSource] I/O seam with no external dependencies,
24+
* making it compatible with JDK 8+ and safe to use from platform threads, virtual threads, Kotlin
25+
* coroutines, and reactive schedulers. Reading from [source] performs blocking I/O; callers in
2726
* non-blocking contexts should dispatch to an appropriate scheduler (e.g., `Dispatchers.IO`,
2827
* `Schedulers.boundedElastic()`).
2928
*
3029
* ## Thread safety
3130
*
32-
* Instances are **not** thread-safe. The stream returned by [byteStream] should be read
33-
* from a single thread only. For concurrent access, wrap with
34-
* [LoggableResponseBody] which
35-
* buffers the content and provides thread-safe, repeatable reads.
31+
* Instances are **not** thread-safe. The [BufferedSource] returned by [source] should be read
32+
* from a single thread only. For concurrent or repeatable access, wrap with
33+
* [LoggableResponseBody], which buffers the content and provides thread-safe, repeatable reads.
3634
*
3735
* ## Single-use contract
3836
*
39-
* The base `ResponseBody` can only be read once — [byteStream] returns the same stream on
40-
* every call, and once consumed, the bytes are gone. Use [bytes] or [string] for a
41-
* one-shot read, or wrap with `LoggableResponseBody` for repeatable access.
37+
* The base `ResponseBody` can only be read once — [source] returns the same [BufferedSource] on
38+
* every call, and once that source is consumed, the bytes are gone. Wrap with
39+
* [LoggableResponseBody] for repeatable access.
4240
*
4341
* @see LoggableResponseBody for a buffered wrapper that
4442
* supports repeatable reads and non-destructive logging.

sdk-core/src/main/kotlin/org/dexpace/sdk/core/http/response/exception/HttpException.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.dexpace.sdk.core.http.response.exception
99

1010
import org.dexpace.sdk.core.http.common.Headers
11+
import org.dexpace.sdk.core.http.response.Response
1112
import org.dexpace.sdk.core.http.response.ResponseBody
1213
import org.dexpace.sdk.core.http.response.Status
1314
import org.dexpace.sdk.core.io.Buffer
@@ -75,6 +76,18 @@ public abstract class HttpException
7576
cause: Throwable? = null,
7677
public val value: Any? = null,
7778
) : RuntimeException(message ?: defaultMessage(status), cause), Retryable {
79+
/**
80+
* Convenience constructor that unpacks [status], [headers], and [body] from a [response].
81+
* The per-status subclasses expose a `(response, message?, cause?, value?)` surface and
82+
* delegate here. `protected` so it is reachable only from subclasses.
83+
*/
84+
protected constructor(
85+
response: Response,
86+
message: String?,
87+
cause: Throwable?,
88+
value: Any?,
89+
) : this(response.status, response.headers, response.body, message, cause, value)
90+
7891
/**
7992
* Whether this exception represents a retryable condition. Derived from
8093
* [RetryUtils.isRetryable] over [status]'s code so it can never disagree with the

0 commit comments

Comments
 (0)