Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk-core/api/sdk-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,7 @@ public class org/dexpace/sdk/core/http/response/exception/GoneException : org/de
public abstract class org/dexpace/sdk/core/http/response/exception/HttpException : java/lang/RuntimeException, org/dexpace/sdk/core/http/response/exception/Retryable {
public static final field Companion Lorg/dexpace/sdk/core/http/response/exception/HttpException$Companion;
public static final field DEFAULT_SNAPSHOT_BYTES I
protected fun <init> (Lorg/dexpace/sdk/core/http/response/Response;Ljava/lang/String;Ljava/lang/Throwable;Ljava/lang/Object;)V
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
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
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ public class ParsedResponse<out T> internal constructor(
) : Closeable {
private val lock = ReentrantLock()

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

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

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

private sealed class Outcome<out T> {
abstract fun get(): T

class Success<out T>(private val value: T) : Outcome<T>() {
override fun get(): T = value
}

class Failure(private val error: Throwable) : Outcome<Nothing>() {
override fun get(): Nothing = throw error
}
}

public companion object {
/**
* Creates a [ParsedResponse] that parses [response] with [handler] on first access.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,32 @@ import org.dexpace.sdk.core.http.common.MediaType
import org.dexpace.sdk.core.io.BufferedSource
import java.io.Closeable
import java.io.IOException
import java.io.InputStream

/**
* Represents the body of an HTTP response.
*
* A `ResponseBody` provides access to the raw bytes of an HTTP response through [byteStream],
* with convenience methods [bytes] and [string] for common consumption patterns. The body
* **must be closed** after use to release the underlying connection — prefer Kotlin's `use {}`
* or Java's try-with-resources.
* A `ResponseBody` exposes the raw bytes of an HTTP response through a single [source] accessor
* returning a [BufferedSource]. The body **must be closed** after use to release the underlying
* connection — prefer Kotlin's `use {}` or Java's try-with-resources, and close it explicitly even
* when the body is skipped without reading.
*
* This class uses only `java.io` APIs with no external dependencies, making it compatible
* with JDK 8+ and safe to use from platform threads, virtual threads, Kotlin coroutines,
* and reactive schedulers. The underlying [InputStream] performs blocking I/O; callers in
* This class depends only on the SDK's [BufferedSource] I/O seam with no external dependencies,
* making it compatible with JDK 8+ and safe to use from platform threads, virtual threads, Kotlin
* coroutines, and reactive schedulers. Reading from [source] performs blocking I/O; callers in
* non-blocking contexts should dispatch to an appropriate scheduler (e.g., `Dispatchers.IO`,
* `Schedulers.boundedElastic()`).
*
* ## Thread safety
*
* Instances are **not** thread-safe. The stream returned by [byteStream] should be read
* from a single thread only. For concurrent access, wrap with
* [LoggableResponseBody] which
* buffers the content and provides thread-safe, repeatable reads.
* Instances are **not** thread-safe. The [BufferedSource] returned by [source] should be read
* from a single thread only. For concurrent or repeatable access, wrap with
* [LoggableResponseBody], which buffers the content and provides thread-safe, repeatable reads.
*
* ## Single-use contract
*
* The base `ResponseBody` can only be read once — [byteStream] returns the same stream on
* every call, and once consumed, the bytes are gone. Use [bytes] or [string] for a
* one-shot read, or wrap with `LoggableResponseBody` for repeatable access.
* The base `ResponseBody` can only be read once — [source] returns the same [BufferedSource] on
* every call, and once that source is consumed, the bytes are gone. Wrap with
* [LoggableResponseBody] for repeatable access.
*
* @see LoggableResponseBody for a buffered wrapper that
* supports repeatable reads and non-destructive logging.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.dexpace.sdk.core.http.response.exception

import org.dexpace.sdk.core.http.common.Headers
import org.dexpace.sdk.core.http.response.Response
import org.dexpace.sdk.core.http.response.ResponseBody
import org.dexpace.sdk.core.http.response.Status
import org.dexpace.sdk.core.io.Buffer
Expand Down Expand Up @@ -75,6 +76,18 @@ public abstract class HttpException
cause: Throwable? = null,
public val value: Any? = null,
) : RuntimeException(message ?: defaultMessage(status), cause), Retryable {
/**
* Convenience constructor that unpacks [status], [headers], and [body] from a [response].
* The per-status subclasses expose a `(response, message?, cause?, value?)` surface and
* delegate here. `protected` so it is reachable only from subclasses.
*/
protected constructor(
response: Response,
message: String?,
cause: Throwable?,
value: Any?,
) : this(response.status, response.headers, response.body, message, cause, value)

/**
* Whether this exception represents a retryable condition. Derived from
* [RetryUtils.isRetryable] over [status]'s code so it can never disagree with the
Expand Down
Loading
Loading