Skip to content

feat: add a ResponseHandler seam with lazy parse-once ParsedResponse - #96

Merged
OmarAlJarrah merged 5 commits into
mainfrom
feat/response-handler-seam
Jun 16, 2026
Merged

feat: add a ResponseHandler seam with lazy parse-once ParsedResponse#96
OmarAlJarrah merged 5 commits into
mainfrom
feat/response-handler-seam

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

Adds a ResponseHandler<T> seam and a lazy, parse-once ParsedResponse<T>:

  • ResponseHandler<T> maps a raw Response to a typed result; companion factories string() / empty() cover the dependency-free cases.
  • ParsedResponse<T> exposes the raw status/headers/request/protocol without parsing, and value() runs the handler lazily and exactly once — memoized race-safely under a ReentrantLock, with a sentinel so a null result or a thrown failure is also cached (never re-consuming the body). close() forwards to the underlying response.
  • In sdk-serde-jackson, jsonHandler(serde, Class<T>) / jsonHandler(serde, TypeReference<T>) stream the body through the deserializer and surface failures as the SDK serde error type.

New public API; tests cover lazy parsing, parse-once memoization (including a memoized failure), single body consumption, and concurrent first access.

Stacked on #93 (which introduces SerdeException). This PR targets that branch and will automatically retarget to main once #93 merges; reviewing #93 first is recommended.

Closes#36

…cion
Two hardening changes to the serde layer.
1. Stable SPI failure type. The `serde` SPI declared no exception type, so the
Jackson adapter let `com.fasterxml.jackson.*` exceptions escape across the
zero-dependency `Serde` boundary. Callers coding against the SPI could not
catch a stable type, and the abstraction leaked its backing library. Add an
open `SerdeException : RuntimeException` to `sdk-core`'s serde package, plus
`SerializationException` / `DeserializationException` for the write and read
directions. The Jackson adapter now catches `JsonProcessingException` (the
root of Jackson's parse/mapping failure hierarchy) at every SPI method and
rethrows as the matching SDK type with the original chained as the cause. A
genuine stream `IOException` still propagates unchanged, and the buffer
overload's bounds checks remain `IndexOutOfBoundsException`.
2. Strict scalar coercion on the default mapper. Jackson's defaults silently
reshape mismatched scalars: a wire string "5" coerced into a numeric field,
numbers into strings, booleans across types. That masks malformed payloads.
The SDK's default `ObjectMapper` now disables
`MapperFeature.ALLOW_COERCION_OF_SCALARS` and sets per-type coercion to
`Fail` for the cross-shape pairs (string -> int/float/boolean, boolean <->
int, and int/float/boolean -> string), built through
`JsonMapper.builder().withCoercionConfig(...)`. Numeric widening
(int -> floating-point) and correctly typed payloads are unaffected, and the
auto-detect features Kotlin data-class binding relies on are left untouched.
This applies only to the SDK default mapper, never to a caller-supplied one.
This is a pre-1.0 behaviour change: payloads whose JSON shape does not match the
target type now fail instead of binding to a quietly wrong value.
Introduce a raw-vs-parsed response seam so callers can read status and
headers without forcing deserialization, and parse the typed body lazily
and exactly once.
- ResponseHandler<T>: a fun interface mapping a raw Response to a typed
result. The handler owns consuming and closing the body. Dep-free
string() and empty() handlers ship in sdk-core.
- ParsedResponse<T>: pairs a Response with a ResponseHandler and exposes
the raw request/protocol/status/message/headers without parsing. value()
runs the handler on first access and memoizes the outcome — success or
failure — behind a ReentrantLock, so repeated access never re-parses or
re-reads the now-consumed body. A null result memoizes correctly via a
sentinel holder. Closeable, forwarding to the raw response.
- SerdeException: an unchecked failure in the serde package that adapters
translate codec errors into, keeping the concrete codec from leaking past
the Serde seam.
- jsonHandler(serde, Class<T>) and jsonHandler(serde, TypeReference<T>) in
sdk-serde-jackson stream the body through the deserializer, close the
response, and surface deserialization failures (and a missing body) as a
SerdeException with the original cause preserved.
The Jackson tests build response bodies over the I/O seam, so the module
gains a test-only dependency on sdk-io-okio3.
…d handler docs
Remove the unused reified `deserialize` import in JsonResponseHandler — the only
call site binds to the member `deserialize(stream, Class<T>)` overload, so the
extension was dead and ktlint's no-unused-imports broke the build.
Documentation and ergonomics follow-ups:
- ParsedResponse.value() now states that handler failures of any type (commonly
unchecked, e.g. SerdeException from the Jackson handler) propagate and are
memoized, so callers do not assume IOException is the only escape.
- Add a comment explaining the deliberate catch(Throwable) in the memoization
path: the single-use body cannot be re-read, so even an Error is pinned rather
than masked by a re-parse.
- Make ParsedResponse's primary constructor internal (factories `of` /
`parsedWith` remain the entry points), matching the immutable-model convention;
api snapshot regenerated.
- Warn on ResponseHandler.string() that it reads an unbounded body into memory,
unsuitable for untrusted or large payloads.
- Soften the DRAIN_CHUNK_BYTES comment so it cannot drift from the I/O segment
size, and include the target type name in the null-body SerdeException message.
@OmarAlJarrah
OmarAlJarrah changed the base branch from feat/serde-error-and-coercion to mainJune 16, 2026 20:12
…ullability; release empty() drain buffer deterministically
…-seam
# Conflicts:
#	sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonObjectMappers.kt
#	sdk-serde-jackson/src/main/kotlin/org/dexpace/sdk/serde/jackson/JacksonSerde.kt
#	sdk-serde-jackson/src/test/kotlin/org/dexpace/sdk/serde/jackson/JacksonObjectMappersTest.kt
@OmarAlJarrah
OmarAlJarrah merged commit 7b0138b into mainJun 16, 2026
1 check passed
@OmarAlJarrah
OmarAlJarrah deleted the feat/response-handler-seam branch June 16, 2026 20:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a ResponseHandler<T> seam with lazy parse-once ParsedResponse<T>

1 participant

@OmarAlJarrah