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
5 changes: 5 additions & 0 deletions docs/guides/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ try {
| `D1RateLimitException` | Cloudflare returned HTTP 429. |
| `D1QueryException` | D1 accepted the request but the SQL operation failed. |
| `D1BatchException` | At least one batch item failed. |
| `D1RawBatchException` | At least one raw batch item failed. |
| `D1MappingException` | Typed row mapping failed. |
| `D1TimeoutException` | Transport timed out. |
| `D1TransportException` | Network or transport failure. |
Expand All @@ -46,3 +47,7 @@ try {
## Sensitive Data

Exception messages do not include API tokens or SQL parameter values. `D1QueryException.sql()` may expose SQL text for diagnostics, so handle it carefully when SQL text itself is sensitive.

`D1BatchException` and `D1RawBatchException` expose the first failed item through
`failedIndex()`. Their `partialResults()` lists are immutable and include all result items returned
by the API, including items after the first failure.
5 changes: 5 additions & 0 deletions docs/guides/raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ List<D1RawResult> results = d1.rawBatch(

Empty raw batches are rejected.

If an item-level result fails, `rawBatch(...)` throws `D1RawBatchException`. Use
`failedIndex()` to locate the first failed item and `partialResults()` to inspect the immutable list
of raw results returned by D1. HTTP failures and top-level API failures continue to use the standard
`D1ApiException` hierarchy.

## Async Raw Queries

`D1AsyncClient` provides matching asynchronous methods:
Expand Down
21 changes: 20 additions & 1 deletion docs/implementation-requirements/02-models-and-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ D1Exception
│ ├─ D1AuthorizationException
│ ├─ D1RateLimitException
│ ├─ D1QueryException
│ └─ D1BatchException
│ ├─ D1BatchException
│ └─ D1RawBatchException
├─ D1MappingException
├─ D1TimeoutException
└─ D1TransportException
Expand Down Expand Up @@ -217,6 +218,23 @@ Rules:
- `failedIndex()` returns the first failed result index.
- `partialResults()` must be immutable.

## D1RawBatchException

Required API:

```java
public int failedIndex();

public List<D1RawResult> partialResults();
```

Rules:

- `failedIndex()` returns the first failed raw result index.
- `partialResults()` must be immutable.
- Only item-level raw batch failures use `D1RawBatchException`; HTTP and top-level API failures keep
their normal status-based exception mapping.

## D1MappingException

Required API:
Expand Down Expand Up @@ -245,6 +263,7 @@ Rules:
- HTTP 2xx with top-level `success=false`: `D1ApiException` family
- Query result `success=false`: `D1QueryException`
- Batch result with any failed item: `D1BatchException`
- Raw batch result with any failed item: `D1RawBatchException`
- Network failure: `D1TransportException`
- Timeout: `D1TimeoutException`
- Non-JSON error body: `D1ApiException` with `rawBody()`
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Responsibilities:
- Map HTTP status and D1 response failures to public exceptions
- Preserve status code, raw body, errors, and messages
- Create `D1BatchException` for batch partial failures
- Create `D1RawBatchException` for raw batch item failures
- Ensure sensitive values are not included in exception messages

## D1Version
Expand Down
3 changes: 2 additions & 1 deletion docs/implementation-requirements/07-testing-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,9 @@ Error tests:
- HTTP 200 with top-level `success=false` throws D1 exception
- Query result `success=false` throws `D1QueryException`
- Batch partial failure throws `D1BatchException`
- Raw batch item failure throws `D1RawBatchException`
- Non-JSON error body is preserved in `rawBody()`
- JSON parse failure throws `D1ApiException`
- JSON parse failure throws `D1ApiException` with the attempted operation
- Network failure throws `D1TransportException`
- Timeout throws `D1TimeoutException`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
- [ ] 5xx maps to `D1ApiException`.
- [ ] Query result failure maps to `D1QueryException`.
- [ ] Batch partial failure maps to `D1BatchException`.
- [ ] Raw batch item failure maps to `D1RawBatchException`.
- [ ] Mapping failure maps to `D1MappingException`.
- [ ] Timeout maps to `D1TimeoutException`.
- [ ] Transport failure maps to `D1TransportException`.
Expand Down
4 changes: 3 additions & 1 deletion docs/requirements/phase-04-response-errors-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ D1Exception
│ ├─ D1AuthorizationException
│ ├─ D1RateLimitException
│ ├─ D1QueryException
│ └─ D1BatchException
│ ├─ D1BatchException
│ └─ D1RawBatchException
├─ D1MappingException
├─ D1TimeoutException
└─ D1TransportException
Expand All @@ -51,6 +52,7 @@ D1Exception
- HTTP 5xx: `D1ApiException`
- Query result `success=false`: `D1QueryException`
- Batch result with any failed item: `D1BatchException`
- Raw batch result with any failed item: `D1RawBatchException`
- Mapping failure: `D1MappingException`
- Timeout: `D1TimeoutException`
- Network failure: `D1TransportException`
Expand Down
20 changes: 12 additions & 8 deletions src/main/java/io/github/xxvw/cloudflare/d1/D1Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public List<D1Result> batch(List<D1Query> queries) {
if (!isSuccessfulStatus(response.statusCode())) {
throw exceptionFactory.httpException(response, D1Operation.BATCH, null);
}
D1ApiResponseDto apiResponse = parseApiResponse(response);
D1ApiResponseDto apiResponse = parseApiResponse(response, D1Operation.BATCH);
if (apiResponse.success != null && !apiResponse.success) {
throw exceptionFactory.topLevelFailure(
response.statusCode(), response.body(), D1Operation.BATCH, apiResponse, null);
Expand Down Expand Up @@ -455,7 +455,7 @@ public List<D1RawResult> rawBatch(List<D1Query> queries) {
if (!isSuccessfulStatus(response.statusCode())) {
throw exceptionFactory.httpException(response, D1Operation.RAW_BATCH, null);
}
D1RawApiResponseDto apiResponse = parseRawApiResponse(response);
D1RawApiResponseDto apiResponse = parseRawApiResponse(response, D1Operation.RAW_BATCH);
if (apiResponse.success != null && !apiResponse.success) {
throw exceptionFactory.topLevelRawFailure(
response.statusCode(), response.body(), D1Operation.RAW_BATCH, apiResponse, null);
Expand Down Expand Up @@ -497,7 +497,7 @@ private D1Result executeSingle(D1Query query, D1Operation operation) {
if (!isSuccessfulStatus(response.statusCode())) {
throw exceptionFactory.httpException(response, operation, query.sql());
}
D1ApiResponseDto apiResponse = parseApiResponse(response);
D1ApiResponseDto apiResponse = parseApiResponse(response, operation);
if (apiResponse.success != null && !apiResponse.success) {
throw exceptionFactory.topLevelFailure(
response.statusCode(), response.body(), operation, apiResponse, query.sql());
Expand All @@ -515,7 +515,7 @@ private D1RawResult executeRawSingle(D1Query query) {
if (!isSuccessfulStatus(response.statusCode())) {
throw exceptionFactory.httpException(response, D1Operation.RAW, query.sql());
}
D1RawApiResponseDto apiResponse = parseRawApiResponse(response);
D1RawApiResponseDto apiResponse = parseRawApiResponse(response, D1Operation.RAW);
if (apiResponse.success != null && !apiResponse.success) {
throw exceptionFactory.topLevelRawFailure(
response.statusCode(), response.body(), D1Operation.RAW, apiResponse, query.sql());
Expand All @@ -527,27 +527,31 @@ private D1RawResult executeRawSingle(D1Query query) {
return result;
}

private D1ApiResponseDto parseApiResponse(D1HttpResponse response) {
private D1ApiResponseDto parseApiResponse(
D1HttpResponse response,
D1Operation operation) {
try {
return responseParser.parseApiResponse(response.body());
} catch (JsonProcessingException e) {
throw new D1ApiException(
"D1 API response was not valid JSON",
null,
operation,
response.statusCode(),
response.body(),
Collections.<D1ResponseInfo>emptyList(),
Collections.<D1ResponseInfo>emptyList());
}
}

private D1RawApiResponseDto parseRawApiResponse(D1HttpResponse response) {
private D1RawApiResponseDto parseRawApiResponse(
D1HttpResponse response,
D1Operation operation) {
try {
return responseParser.parseRawApiResponse(response.body());
} catch (JsonProcessingException e) {
throw new D1ApiException(
"D1 API response was not valid JSON",
null,
operation,
response.statusCode(),
response.body(),
Collections.<D1ResponseInfo>emptyList(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.xxvw.cloudflare.d1;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
* D1 raw batch failure.
*/
public final class D1RawBatchException extends D1ApiException {
/** Index of the first failed raw batch item. */
private final int failedIndex;
/** Raw result items returned by the API. */
private final List<D1RawResult> partialResults;

/**
* Creates a raw batch exception.
*
* @param statusCode HTTP status code
* @param rawBody raw HTTP response body
* @param errors API errors for the failed item
* @param messages API messages for the failed item
* @param failedIndex index of the first failed raw batch item
* @param partialResults raw result items returned before failure handling
*/
public D1RawBatchException(
Integer statusCode,
String rawBody,
List<D1ResponseInfo> errors,
List<D1ResponseInfo> messages,
int failedIndex,
List<D1RawResult> partialResults) {
super("D1 raw batch failed", D1Operation.RAW_BATCH, statusCode, rawBody, errors, messages);
this.failedIndex = failedIndex;
this.partialResults = partialResults == null
? Collections.<D1RawResult>emptyList()
: Collections.unmodifiableList(new ArrayList<>(partialResults));
}

/**
* Index of the first failed raw batch item.
*
* @return failed item index
*/
public int failedIndex() {
return failedIndex;
}

/**
* Raw result items returned by the API.
*
* @return immutable partial results
*/
public List<D1RawResult> partialResults() {
return partialResults;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.github.xxvw.cloudflare.d1.D1BatchException;
import io.github.xxvw.cloudflare.d1.D1Operation;
import io.github.xxvw.cloudflare.d1.D1QueryException;
import io.github.xxvw.cloudflare.d1.D1RawBatchException;
import io.github.xxvw.cloudflare.d1.D1RawResult;
import io.github.xxvw.cloudflare.d1.D1RateLimitException;
import io.github.xxvw.cloudflare.d1.D1ResponseInfo;
Expand Down Expand Up @@ -110,21 +111,25 @@ public D1BatchException batchFailure(int statusCode, String rawBody, List<D1Resu
results);
}

public D1ApiException rawBatchFailure(int statusCode, String rawBody, List<D1RawResult> results) {
D1RawResult failed = null;
for (D1RawResult result : results) {
if (!result.success()) {
failed = result;
public D1RawBatchException rawBatchFailure(
int statusCode,
String rawBody,
List<D1RawResult> results) {
int failedIndex = 0;
for (int i = 0; i < results.size(); i++) {
if (!results.get(i).success()) {
failedIndex = i;
break;
}
}
return new D1ApiException(
"D1 raw batch failed",
D1Operation.RAW_BATCH,
D1RawResult failed = results.isEmpty() ? null : results.get(failedIndex);
return new D1RawBatchException(
statusCode,
rawBody,
failed == null ? Collections.<D1ResponseInfo>emptyList() : failed.errors(),
failed == null ? Collections.<D1ResponseInfo>emptyList() : failed.messages());
failed == null ? Collections.<D1ResponseInfo>emptyList() : failed.messages(),
failedIndex,
results);
}

private D1ApiException statusException(
Expand Down
19 changes: 14 additions & 5 deletions src/test/java/io/github/xxvw/cloudflare/d1/D1AsyncClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,26 @@ void asyncBatchPartialFailurePropagatesBatchExceptionDetails() {
@Test
void executeAndRawBatchAsyncFailuresPropagatePublicExceptions() {
server.enqueue(jsonError(500));
server.enqueue(ok("{\"success\":true,\"result\":[{\"success\":false,"
+ "\"results\":{\"columns\":[],\"rows\":[]},\"errors\":[{\"code\":3,\"message\":\"batch\"}]}],"
+ "\"errors\":[],\"messages\":[]}"));
server.enqueue(ok("{\"success\":true,\"result\":["
+ "{\"success\":true,\"results\":{\"columns\":[\"value\"],\"rows\":[[1]]}},"
+ "{\"success\":false,\"results\":{\"columns\":[],\"rows\":[]},"
+ "\"errors\":[{\"code\":3,\"message\":\"batch\"}]}"
+ "],\"errors\":[],\"messages\":[]}"));
D1AsyncClient client = testClient();

assertThatThrownBy(() -> client.executeAsync("UPDATE users SET active = ?", false).join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(D1ApiException.class);
assertThatThrownBy(() -> client.rawBatchAsync(D1Query.of("SELECT 1")).join())
CompletableFuture<List<D1RawResult>> rawBatchFailure =
client.rawBatchAsync(D1Query.of("SELECT 1"), D1Query.of("SELECT 2"));
assertThatThrownBy(rawBatchFailure::join)
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(D1ApiException.class);
.hasCauseInstanceOf(D1RawBatchException.class);
D1RawBatchException cause =
(D1RawBatchException) rawBatchFailure.handle((value, error) -> error.getCause()).join();
assertThat(cause.failedIndex()).isEqualTo(1);
assertThat(cause.partialResults()).hasSize(2);
assertThat(cause.errors()).extracting(D1ResponseInfo::code).containsExactly(3);
}

@Test
Expand Down
Loading