Skip to content

Commit 28a0261

Browse files
committed
Merge remote-tracking branch 'origin/main' into refactor/error-handling-unification
# Conflicts: # README.md
2 parents cf7e498 + 26578ba commit 28a0261

80 files changed

Lines changed: 6312 additions & 550 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: CI
2+
3+
# Run the full Gradle build on every pull request and on pushes to main.
4+
# `./gradlew build` runs the complete quality gate: tests, ktlint, detekt,
5+
# apiCheck (binary-compatibility), explicit-API strict mode, allWarningsAsErrors,
6+
# and the aggregate 80% Kover line-coverage floor.
7+
on:
8+
pull_request:
9+
push:
10+
branches: [main]
11+
12+
# Cancel superseded runs on the same ref so only the latest commit of a branch/PR builds.
13+
concurrency:
14+
group: ci-${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
build:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- name: Check out repository
25+
uses: actions/checkout@v4
26+
with:
27+
# The styleguide directory is a git submodule; fetch it so the working tree matches local checkouts.
28+
submodules: recursive
29+
30+
# Install a single Temurin JDK 21. The build's Gradle daemon runs on this JVM, and the
31+
# foojay-resolver-convention plugin (settings.gradle.kts) auto-provisions the JDK 8 and
32+
# JDK 11 toolchains the Java-8 and JDK-11 modules require. JDK 21 also satisfies the
33+
# virtual-threads module's toolchain. JDK 21 (rather than the newest LTS) is deliberate:
34+
# detekt 1.23.x crashes on JDK 25+, so keeping the daemon on 21 keeps the detekt gate green.
35+
- name: Set up JDK 21
36+
uses: actions/setup-java@v4
37+
with:
38+
distribution: temurin
39+
java-version: '21'
40+
41+
# Built-in Gradle dependency and build caching, plus wrapper validation.
42+
- name: Set up Gradle
43+
uses: gradle/actions/setup-gradle@v4
44+
45+
- name: Build
46+
run: ./gradlew build

AUDIT.md

Lines changed: 149 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
<h1 align="center">Java SDKs Platform</h1>
99

10+
[![CI](https://github.com/dexpace/java-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/dexpace/java-sdk/actions/workflows/ci.yml)
1011
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
1112
[![Kotlin](https://img.shields.io/badge/kotlin-2.3.21-7F52FF.svg?logo=kotlin&logoColor=white)](https://kotlinlang.org)
1213
![JDK](https://img.shields.io/badge/JDK-8%2B-437291.svg?logo=openjdk&logoColor=white)
@@ -246,7 +247,7 @@ See [docs/pipelines.md](docs/pipelines.md) for the step-author walkthrough.
246247
|---|---|
247248
| `client` | `HttpClient`, `AsyncHttpClient` — the two transport SPIs (sync and async). |
248249
| `http.request` | `Request`, `RequestBody`, `FileRequestBody`, `LoggableRequestBody`, `Method`. |
249-
| `http.response` | `Response`, `ResponseBody`, `LoggableResponseBody`, `Status` (a value-carrying class with a total `fromCode`). |
250+
| `http.response` | `Response`, `ResponseBody`, `LoggableResponseBody`, `Status` (a value-carrying class with a total `fromCode`), plus the raw-vs-parsed seam: `ResponseHandler<T>` (with dep-free `string()`/`empty()` handlers) and a lazy, parse-once `ParsedResponse<T>`. |
250251
| `http.response.exception` | Typed `HttpException` hierarchy (`BadRequestException`, `RequestTimeoutException`, `TooManyRequestsException`, `ServiceUnavailableException`, …) with `isRetryable` derived from `RetryUtils.isRetryable` and exposed via the `Retryable` interface, plus `NetworkException` and `HttpExceptionFactory`. |
251252
| `http.common` | `Headers`, `HttpHeaderName` (interned), `MediaType`, `Protocol`, `HttpRange`, `ETag`, `RequestConditions`. |
252253
| `http.context` | `CallContext``DispatchContext``RequestContext``ExchangeContext` chain, `ContextStore`. |
@@ -255,16 +256,19 @@ See [docs/pipelines.md](docs/pipelines.md) for the step-author walkthrough.
255256
| `http.auth` | `Credential` sealed hierarchy (`KeyCredential`, `NamedKeyCredential`, `BearerToken`), `BearerTokenProvider`, `AuthScheme`, `AuthMetadata`, RFC 7235 challenge parser, `BasicChallengeHandler`, `DigestChallengeHandler`, `CompositeChallengeHandler`. |
256257
| `http.sse` | `ServerSentEventReader` (WHATWG spec), `ServerSentEvent`, `ServerSentEventListener`, `BufferedSource.readServerSentEvents()`. |
257258
| `http.paging` | `PagedIterable<T>`, `PagedResponse<T>`, `PagingOptions` with `byPage()` and `stream()` accessors. |
258-
| `pagination` | `Paginator<T>` (with a `maxPages` safety cap) over cursor / page-number / token / link-header `PaginationStrategy` implementations, plus `Page<T>` / `SimplePage<T>`. |
259+
| `pagination` | `Paginator<T>` (with a `maxPages` safety cap) over cursor / page-number / link-header `PaginationStrategy` implementations, plus `Page<T>` / `SimplePage<T>`. Token-style APIs use `CursorPaginationStrategy` with the query-param name set (e.g. `"page_token"`). |
259260
| `pipeline` | Recovery-aware primitives: `RequestPipeline`, `ResponsePipeline`, `ExecutionPipeline` over a sealed `ResponseOutcome`, with steps (`pipeline.step`, `pipeline.step.retry`) like `RetryStep`, `ResponseRecoveryStep`, `IdempotencyKeyStep`, `ClientIdentityStep`. |
260-
| `serde` | `Serde`, `Serializer`, `Deserializer` abstractions and `Tristate<T>` (absent / null / present). |
261+
| `serde` | `Serde`, `Serializer`, `Deserializer` abstractions, `Tristate<T>` (absent / null / present), and `SerdeException` (the unchecked failure adapters translate codec errors into). |
261262
| `io` | `Source`, `Sink`, `Buffer`, `BufferedSource`, `BufferedSink`, `IoProvider`, `Io`, `TeeSink`. |
262263
| `instrumentation` | `ClientLogger` (zero-alloc disabled path), `LoggingEvent`, `UrlRedactor`, `Tracer` / `NoopTracer`, `Span` / `NoopSpan`, `InstrumentationContext`. |
263264
| `instrumentation.metrics` | `Meter`, `LongCounter`, `DoubleHistogram`, `NoopMeter`. |
264265
| `config` | `Configuration` (system-property + env-var layered lookup), `ConfigurationBuilder`. |
265266
| `util` | `Clock`, `Uuids` (non-blocking v4), `DateTimeRfc1123`, `RetryUtils`, `ProxyOptions`, `Futures`. |
266267
| `generics` | `Builder<T>` — the generic builder interface every SDK builder implements. |
267268

269+
Token-style APIs (`next_page_token`, `pageToken`, …) are served by `CursorPaginationStrategy`:
270+
construct it with the desired query-param name, e.g. `CursorPaginationStrategy(items, extractor, "page_token")`.
271+
268272
## Building
269273

270274
```bash

docs/architecture.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ Shipped pillar/step implementations live in `http.pipeline.steps`: `DefaultRedir
271271
`DefaultRetryStep`, `AuthStep` (+ `BearerTokenAuthStep` / `KeyCredentialAuthStep`),
272272
`DefaultInstrumentationStep`, and the redirect/retry option types.
273273

274+
For why this layer uses ordered stages with pillar-uniqueness rather than nested `HttpClient`
275+
decorators — and the one cost that buys (the `next.copy()` re-drive contract) — see
276+
[Pipeline Mechanism](pipelines.md#why-ordered-stages-not-nested-decorators).
277+
274278
#### Recovery-aware primitives (`org.dexpace.sdk.core.pipeline`)
275279

276280
A lower-level layer that threads a sealed `ResponseOutcome` so recovery steps observe and
@@ -346,9 +350,12 @@ Two complementary surfaces for walking multi-page responses.
346350
|-----------------------------------------------------------------|-----------------------------------------------------------------------|
347351
| `Paginator<T>` | Lazily iterates pages by re-issuing requests through an `HttpClient`; carries a `maxPages` safety cap |
348352
| `PaginationStrategy<T>` | Computes the next-page request (or stops) from the current page |
349-
| `CursorPaginationStrategy` / `PageNumberPaginationStrategy` / `TokenPaginationStrategy` / `LinkHeaderPaginationStrategy` | The four shipped strategies |
353+
| `CursorPaginationStrategy` / `PageNumberPaginationStrategy` / `LinkHeaderPaginationStrategy` | The shipped strategies |
350354
| `PagedIterable<T>` | First/next-page fetcher abstraction over `PagedResponse`, with its own `maxPages` cap |
351355

356+
Token-style APIs (`next_page_token`, `pageToken`, …) are handled by `CursorPaginationStrategy`
357+
constructed with the query-param name set (e.g. `"page_token"`), so no separate token strategy is needed.
358+
352359
### Serialization
353360

354361
**Package**: `org.dexpace.sdk.core.serde`

docs/http-body-logging-and-concurrency.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and the concurrency decisions behind them.
1414
- [Architecture](#architecture)
1515
- [LoggableRequestBody — Tee-Write Strategy](#loggablerequestbody--tee-write-strategy)
1616
- [LoggableResponseBody — Drain-Once Strategy](#loggableresponsebody--drain-once-strategy)
17+
- [Logged body size vs. the body the consumer receives](#logged-body-size-vs-the-body-the-consumer-receives)
1718
- [Reading a Snapshot](#reading-a-snapshot)
1819
- [Internal Stream Utilities](#internal-stream-utilities)
1920
- [Concurrency Design](#concurrency-design)
@@ -240,6 +241,45 @@ retains whatever bytes were read before the failure and caches the exception:
240241
post-mortem logging that records "what we got" alongside the exception.
241242
- `captureException` surfaces the cached exception (or `null`) without triggering a drain.
242243

244+
### Logged body size vs. the body the consumer receives
245+
246+
When `HttpLogLevel.BODY_AND_HEADERS` is enabled, the instrumentation step
247+
(`DefaultInstrumentationStep` / `DefaultAsyncInstrumentationStep`) wraps the response body in a
248+
`LoggableResponseBody` bounded to `HttpInstrumentationOptions.bodyPreviewMaxBytes` (default
249+
8 KiB, `DEFAULT_BODY_PREVIEW_MAX_BYTES`). Two consequences follow that are easy to miss when
250+
reading the logs:
251+
252+
**1. The body delivered downstream can be larger than the logged preview.** The cap bounds only
253+
the in-memory *capture*, not the body. For a response larger than `bodyPreviewMaxBytes`, the
254+
step buffers the preview prefix and the wrapper then streams the full body to the consumer — it
255+
replays the captured prefix and continues from the live tail (see the bounded-capture diagram
256+
above). The preview you see in the log is a prefix; the consumer still reads every byte.
257+
258+
**2. The logged size fields measure different things.** The step emits two size-related fields
259+
on the `http.response` event, and they are not the same number for an over-cap body:
260+
261+
| Field | Source | What it reports |
262+
|---------------------------|----------------------------------------------|---------------------------------------------------------------------------------|
263+
| `response.body.size` | `loggableBody.snapshot(bodyPreviewMaxBytes)` | Size of the **captured preview** — bounded by `bodyPreviewMaxBytes` |
264+
| `response.body.preview` | the same captured bytes, decoded as UTF-8 | The preview text (a prefix for an over-cap body) |
265+
| `response.content.length` | `response.body.contentLength()` | The body's **true** length when the origin declared one (`Content-Length`); `-1` for unknown-length / streaming bodies |
266+
267+
So `response.body.size` is the *captured/preview* size, **not** necessarily the full body size.
268+
When a body exceeds the cap, `response.body.size` saturates near `bodyPreviewMaxBytes` while
269+
`response.content.length` still shows the real length. Read `content.length` (not
270+
`body.size`) when you need the full size, and treat `body.preview` as a prefix that may be
271+
truncated. The two agree only when the whole body fit within the cap — exactly the case where
272+
`contentLength()` itself returns the captured size (see **`contentLength()`** above).
273+
274+
**Streaming / unknown-length bodies (async path).** `DefaultAsyncInstrumentationStep` skips the
275+
capture entirely when `contentLength() < 0`, because the bounded drain would run on the
276+
future-completion thread and a slow producer could stall it. Such bodies stream to the consumer
277+
unwrapped, so they carry **no** `response.body.size` / `response.body.preview` fields at all —
278+
absence of those fields is expected for chunked or streaming responses, not a logging bug. The
279+
synchronous `DefaultInstrumentationStep` drains known-length and unknown-length bodies alike (it
280+
runs on the caller's thread), but the size-vs-preview distinction above applies to it just the
281+
same.
282+
243283
### Reading a Snapshot
244284

245285
The only logging output is a raw `ByteArray`:

docs/implementation-plan.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,8 @@ defaults (per Square: `FAIL_ON_UNKNOWN_PROPERTIES=false`, `WRITE_DATES_AS_TIMEST
372372

373373
### WU-9: Pagination primitives
374374

375-
**Status: shipped.** `Page`, `Paginator`, `PaginationStrategy`, and the four strategies
376-
(`Cursor` / `PageNumber` / `LinkHeader` / `Token`) are in `sdk-core/.../pagination`, alongside
375+
**Status: shipped.** `Page`, `Paginator`, `PaginationStrategy`, and the three strategies
376+
(`Cursor` / `PageNumber` / `LinkHeader`) are in `sdk-core/.../pagination`, alongside
377377
helper types `SimplePage` and `RequestRebuilder`. `Paginator` gained a `maxPages` safety cap
378378
(default `Long.MAX_VALUE`) beyond the original sketch, to bound runaway iteration against servers
379379
that never advance their cursor.
@@ -390,7 +390,6 @@ link-header strategies without over-engineering. Sync first; async adapter follo
390390
- `CursorPaginationStrategy<T>(cursorPath, itemsPath, parser)` — read `next_cursor` from body
391391
- `PageNumberPaginationStrategy<T>(pageParam, itemsPath, parser)` — increment page number
392392
- `LinkHeaderPaginationStrategy<T>(itemsPath, parser)` — RFC 5988 `Link: <url>; rel="next"`
393-
- `TokenPaginationStrategy<T>(tokenPath, tokenParam, itemsPath, parser)` — token in body, sent as query param
394393
- `sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/PaginatorTests.kt` (test) — table-driven tests against MockWebServer fixtures.
395394

396395
**Acceptance criteria:**

0 commit comments

Comments
 (0)