Skip to content

fix(api): handle kobo proxy errors during kobo sync#217

Merged
balazs-szucs merged 4 commits into
grimmory-tools:developfrom
imnotjames:fix/216/handle-kobo-proxy-failures
Mar 27, 2026
Merged

fix(api): handle kobo proxy errors during kobo sync#217
balazs-szucs merged 4 commits into
grimmory-tools:developfrom
imnotjames:fix/216/handle-kobo-proxy-failures

Conversation

@imnotjames

@imnotjames imnotjames commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Description

Whenever Kobo servers error out the kobo sync fails to process with an HTTP 500. This is because Kobo servers respond with HTML. In the sync routes we try to combine the kobo response with our response whcih fails.

This corrects the issue by wrapping the sync code that makes a proxy request in a try / catch & handling JSON parse failures in the kobo proxy requests more gracefully.

Linked Issue: Fixes #216

Changes

  • updates kobo sync to handle failures more gracefully (like the kobo init endpoint)
  • tells kobo that we only ever want JSON - including in error messages (otherwise they send HTML and we fail to parse it)

Testing Strategy

Tested with a Kobo Clara BW to do standard sync operations & file downloading.

Summary by CodeRabbit

  • Bug Fixes

    • Sync is more resilient: remote parsing issues now log and return an empty result so they don't break sync.
    • Proxy errors during sync are caught and logged so they no longer cause incomplete state updates.
  • Refactor

    • Response and header handling tightened: outbound requests explicitly request JSON; sync-token and continue-sync decisions now update only when upstream headers are present.

@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Add a helper to map Kobo JSON responses into Entitlement objects, make proxied requests set Accept: application/json, and guard the proxy call plus header parsing so proxy errors or non-JSON responses are logged and do not abort the sync flow.

Changes

Cohort / File(s) Summary
Kobo Sync Service
booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
Added private getEntitlementsFromKoboStoreResponse(ResponseEntity<JsonNode>) to map Kobo JSON into Entitlement instances (returns empty list on null body or mapping exceptions). Wrapped koboServerProxy.proxyCurrentRequest(...) and upstream header reads (X_KOBO_SYNC, X_KOBO_SYNCTOKEN) in try/catch; when the proxy response is null/unusable the service logs and skips proxy-derived entitlement/header updates.
Kobo Server Proxy
booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
Removed Accept from the inbound-header whitelist so it is not forwarded; outbound proxied requests now explicitly set Accept: application/json (and keep Content-Type: application/json). Proxy now avoids attempting to parse non-JSON error bodies as JSON when upstream responses are unusable.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SyncService as KoboLibrarySyncService
  participant Proxy as KoboServerProxy
  participant Kobo

  Client->>SyncService: POST /api/.../library/sync
  SyncService->>Proxy: proxyCurrentRequest(..., includeBody=true)
  Proxy->>Kobo: HTTP request (Accept: application/json, Content-Type: application/json)
  Kobo-->>Proxy: HTTP response (JSON 200 or error/non-JSON)
  Proxy-->>SyncService: ResponseEntity<JsonNode> or null on parse/error
  alt Proxy response usable (JSON)
    SyncService->>SyncService: getEntitlementsFromKoboStoreResponse(response)
    SyncService->>SyncService: read X_KOBO_SYNC / X_KOBO_SYNCTOKEN headers
  else Proxy threw / response unusable
    SyncService->>SyncService: log warning/error, skip proxy-derived updates
  end
  SyncService-->>Client: continue sync flow (no fatal proxy error)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through JSON, tidy and keen,
I catch the errors you might not have seen.
When Kobo sends HTML and starts to balk,
I log a note and keep the sync on walk.
🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows the conventional commit format with prefix 'fix', scope 'api', and descriptive message about handling Kobo proxy errors during sync.
Description check ✅ Passed The PR description includes all required template sections: description explaining the problem, linked issue reference (#216), and detailed changes addressing the Kobo proxy error handling.
Linked Issues check ✅ Passed The code changes directly address all acceptance criteria from issue #216: wrapping proxy calls in try/catch to handle failures gracefully, setting Accept header to request JSON responses, and preventing 500 errors when Kobo returns HTML.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #216: KoboLibrarySyncService adds fault-tolerant proxy handling with exception catching, and KoboServerProxy adds Accept header to request JSON responses.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Comment @coderabbitai help to get the list of available commands and usage tips.

@imnotjames imnotjames changed the title Fix/216/handle kobo proxy failures fix(api): handle kobo proxy failures during kobo sync Mar 26, 2026
@imnotjames imnotjames changed the title fix(api): handle kobo proxy failures during kobo sync fix(api): handle kobo proxy errors during kobo sync Mar 26, 2026
@imnotjames imnotjames marked this pull request as ready for review March 26, 2026 04:51
@imnotjames imnotjames force-pushed the fix/216/handle-kobo-proxy-failures branch from d5e3714 to a7d8d9a Compare March 26, 2026 04:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java (1)

108-109: Potential duplicate Accept header when original request includes one.

HttpHeaders.ACCEPT is in HEADERS_OUT_INCLUDE (line 44), so the forwarding loop (lines 111-117) will add the original request's Accept header after this hardcoded one. Since HttpRequest.Builder.header() appends rather than replaces, the outbound request may carry both values.

Consider either:

  1. Removing HttpHeaders.ACCEPT from HEADERS_OUT_INCLUDE so only the hardcoded header is sent, or
  2. Setting the Accept header after the forwarding loop to ensure it takes precedence.
Option 1: Exclude Accept from forwarding
 private static final Set<String> HEADERS_OUT_EXCLUDE = Set.of(
-        KoboHeaders.X_KOBO_SYNCTOKEN
+        KoboHeaders.X_KOBO_SYNCTOKEN,
+        HttpHeaders.ACCEPT
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`
around lines 108 - 109, The hardcoded Accept header appended via
HttpRequest.Builder.header(HttpHeaders.ACCEPT, "application/json") can be
duplicated because HEADERS_OUT_INCLUDE contains HttpHeaders.ACCEPT and the
forwarding loop copies original headers; to fix, either remove
HttpHeaders.ACCEPT from the HEADERS_OUT_INCLUDE collection or move/set the
Accept header after the forwarding loop so it replaces/overrides the forwarded
value; update the code around HEADERS_OUT_INCLUDE usage and the
HttpRequest.Builder.header(...) call in KoboServerProxy (the header-forwarding
loop and the builder.accept/header call) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`:
- Around line 108-109: The hardcoded Accept header appended via
HttpRequest.Builder.header(HttpHeaders.ACCEPT, "application/json") can be
duplicated because HEADERS_OUT_INCLUDE contains HttpHeaders.ACCEPT and the
forwarding loop copies original headers; to fix, either remove
HttpHeaders.ACCEPT from the HEADERS_OUT_INCLUDE collection or move/set the
Accept header after the forwarding loop so it replaces/overrides the forwarded
value; update the code around HEADERS_OUT_INCLUDE usage and the
HttpRequest.Builder.header(...) call in KoboServerProxy (the header-forwarding
loop and the builder.accept/header call) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b537e3a-3add-4346-af37-d81b1a6c888d

📥 Commits

Reviewing files that changed from the base of the PR and between 3f2c0c5 and a7d8d9a.

📒 Files selected for processing (2)
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Packaging Smoke Test
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🧠 Learnings (4)
📓 Common learnings
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.
📚 Learning: 2026-03-26T02:20:22.468Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 198
File: booklore-api/src/main/java/org/booklore/controller/KoboController.java:100-103
Timestamp: 2026-03-26T02:20:22.468Z
Learning: In `booklore-api/src/main/java/org/booklore/controller/KoboController.java`, the Kobo reader device already makes direct outbound connections to Kobo's CDN/servers for other requests (confirmed by imnotjames via testing). Therefore, returning a `307 TEMPORARY_REDIRECT` to `cdn.kobo.com` for non-`BL-` book cover thumbnails (instead of server-side proxying) does NOT introduce a new privacy or behavioral regression — the Kobo device's IP and User-Agent are already exposed to Kobo through other direct requests.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🔇 Additional comments (2)
booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java (2)

42-65: Clean extraction of entitlement mapping logic.

The helper method properly handles null body, iterates through the array, and returns an empty collection on failure. The logging for unknown entitlement types at line 54 is helpful for debugging.


138-151: Graceful error handling aligns with PR objectives.

The try/catch wrapper correctly isolates the Kobo proxy call, allowing sync to complete with local entitlements when upstream fails. The warning-level log at line 150 is appropriate since this is a recoverable condition.

One minor note: if tokenGenerator.fromBase64() (line 148) throws after entitlements.addAll() (line 141) succeeds, the response will include Kobo entitlements but with a stale syncToken. This is acceptable since the next sync will re-request from Kobo.

@imnotjames imnotjames marked this pull request as draft March 26, 2026 05:12
@imnotjames imnotjames force-pushed the fix/216/handle-kobo-proxy-failures branch from a7d8d9a to 22a9b24 Compare March 26, 2026 05:14
@imnotjames imnotjames marked this pull request as ready for review March 26, 2026 05:23
@imnotjames imnotjames force-pushed the fix/216/handle-kobo-proxy-failures branch from 5d6e4ce to 4320481 Compare March 26, 2026 05:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java (1)

45-61: Keep one bad entitlement from dropping the whole upstream payload.

The outer catch makes this helper all-or-nothing: if one treeToValue(...) fails, every entitlement already parsed from the response is discarded. Since this branch is now best-effort anyway, it would be safer to log and skip only the malformed item.

Possible refactor
-                .map(body -> {
-                    try {
-                        List<Entitlement> results = new ArrayList<>();
-                        if (body.isArray()) {
-                            for (JsonNode node : body) {
-                                if (node.has("NewEntitlement")) {
-                                    results.add(objectMapper.treeToValue(node, NewEntitlement.class));
-                                } else if (node.has("ChangedEntitlement")) {
-                                    results.add(objectMapper.treeToValue(node, ChangedEntitlement.class));
-                                } else {
-                                    log.warn("Unknown entitlement type in Kobo response: {}", node);
-                                }
-                            }
-                        }
-                        return results;
-                    } catch (Exception e) {
-                        log.error("Failed to map Kobo response to Entitlement objects", e);
-                        return Collections.<Entitlement>emptyList();
-                    }
-                })
+                .map(body -> {
+                    List<Entitlement> results = new ArrayList<>();
+                    if (body.isArray()) {
+                        for (JsonNode node : body) {
+                            try {
+                                if (node.has("NewEntitlement")) {
+                                    results.add(objectMapper.treeToValue(node, NewEntitlement.class));
+                                } else if (node.has("ChangedEntitlement")) {
+                                    results.add(objectMapper.treeToValue(node, ChangedEntitlement.class));
+                                } else {
+                                    log.warn("Unknown entitlement type in Kobo response: {}", node);
+                                }
+                            } catch (Exception e) {
+                                log.warn("Skipping malformed entitlement from Kobo response: {}", node, e);
+                            }
+                        }
+                    }
+                    return results;
+                })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`
around lines 45 - 61, The current mapping in KoboLibrarySyncService swallows all
parsed entitlements if any single objectMapper.treeToValue(...) throws; change
the logic so each JsonNode is parsed inside its own try/catch (around the
objectMapper.treeToValue call) and on parse failure log the node and exception
(e.g., log.warn or log.error with the node and e) and continue, adding only
successfully parsed NewEntitlement or ChangedEntitlement instances to the
results List<Entitlement>; keep or narrow any outer catch for truly unexpected
failures but do not return Collections.emptyList() just because one item failed
so that valid entitlements already parsed are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`:
- Around line 143-149: The code updates shouldContinueSync before decoding the
sync token, allowing a failed token parse (tokenGenerator.fromBase64) to leave
shouldContinueSync set to "continue"; change the order and logic in the try
block in KoboLibrarySyncService so you first read and decode the
X_KOBO_SYNCTOKEN into syncToken (using tokenGenerator.fromBase64 and only
assigning to syncToken on success), and only after the token decode succeeds
assign shouldContinueSync from
koboStoreResponse.getHeaders().getFirst(KoboHeaders.X_KOBO_SYNC); ensure
exceptions from tokenGenerator.fromBase64 prevent mutating shouldContinueSync
and are caught as before.

In `@booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`:
- Around line 107-108: The Accept: application/json header is being forced for
all proxied requests in proxyCurrentRequest(), which breaks non-JSON endpoints;
change the code so the Accept header is only set to "application/json" when
proxying the specific sync/init routes (e.g., detect request.getRequestURI() or
path info contains "/api/v2/sync" or "/api/v2/sync/init") or when the incoming
request has no Accept header. Update proxyCurrentRequest() to conditionally add
.header(HttpHeaders.ACCEPT, "application/json") only for those sync/init paths
(leave other requests to forward the original Accept or omit it).

---

Nitpick comments:
In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`:
- Around line 45-61: The current mapping in KoboLibrarySyncService swallows all
parsed entitlements if any single objectMapper.treeToValue(...) throws; change
the logic so each JsonNode is parsed inside its own try/catch (around the
objectMapper.treeToValue call) and on parse failure log the node and exception
(e.g., log.warn or log.error with the node and e) and continue, adding only
successfully parsed NewEntitlement or ChangedEntitlement instances to the
results List<Entitlement>; keep or narrow any outer catch for truly unexpected
failures but do not return Collections.emptyList() just because one item failed
so that valid entitlements already parsed are preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 152a6d63-6c5c-4df2-bbbc-442bd5c3b986

📥 Commits

Reviewing files that changed from the base of the PR and between a7d8d9a and 4320481.

📒 Files selected for processing (2)
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🧠 Learnings (3)
📓 Common learnings
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-26T02:20:22.468Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 198
File: booklore-api/src/main/java/org/booklore/controller/KoboController.java:100-103
Timestamp: 2026-03-26T02:20:22.468Z
Learning: In `booklore-api/src/main/java/org/booklore/controller/KoboController.java`, the Kobo reader device already makes direct outbound connections to Kobo's CDN/servers for other requests (confirmed by imnotjames via testing). Therefore, returning a `307 TEMPORARY_REDIRECT` to `cdn.kobo.com` for non-`BL-` book cover thumbnails (instead of server-side proxying) does NOT introduce a new privacy or behavioral regression — the Kobo device's IP and User-Agent are already exposed to Kobo through other direct requests.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java

Comment thread booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java Outdated
@imnotjames imnotjames force-pushed the fix/216/handle-kobo-proxy-failures branch 2 times, most recently from 59ab0db to a77f813 Compare March 26, 2026 16:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java (1)

151-155: ⚠️ Potential issue | 🟠 Major

Keep upstream sync-header parsing non-fatal to avoid reintroducing 500s.

At Line 152, tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader) can throw and currently escapes, which can still abort sync despite the new best-effort proxy guard. This should be contained so proxy/header-parse issues remain non-fatal.

💡 Suggested defensive fix
-                if (upstreamKoboSyncTokenHeader != null) {
-                    syncToken = tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader);
-                }
-
-                shouldContinueSync = "continue".equalsIgnoreCase(upstreamContinueSyncHeader);
+                try {
+                    if (upstreamKoboSyncTokenHeader != null) {
+                        BookloreSyncToken decoded = tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader);
+                        if (decoded != null) {
+                            syncToken = decoded;
+                        }
+                    }
+                    shouldContinueSync = "continue".equalsIgnoreCase(upstreamContinueSyncHeader);
+                } catch (Exception e) {
+                    log.warn("Failed to parse upstream Kobo sync headers; continuing with local sync state", e);
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`
around lines 151 - 155, Parsing the upstream sync token with
tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader) can throw and currently
lets exceptions escape; make header parsing non-fatal by wrapping the call in a
try/catch that logs a debug/warn (including the header value or parse error) and
leaves syncToken null/unchanged if parsing fails, and similarly defensively
parse upstreamContinueSyncHeader into shouldContinueSync so header parse errors
cannot abort the sync flow (use upstreamKoboSyncTokenHeader,
tokenGenerator.fromBase64, syncToken, upstreamContinueSyncHeader,
shouldContinueSync to locate the spots).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`:
- Around line 151-155: Parsing the upstream sync token with
tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader) can throw and currently
lets exceptions escape; make header parsing non-fatal by wrapping the call in a
try/catch that logs a debug/warn (including the header value or parse error) and
leaves syncToken null/unchanged if parsing fails, and similarly defensively
parse upstreamContinueSyncHeader into shouldContinueSync so header parse errors
cannot abort the sync flow (use upstreamKoboSyncTokenHeader,
tokenGenerator.fromBase64, syncToken, upstreamContinueSyncHeader,
shouldContinueSync to locate the spots).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1bf0497-aedc-4429-a4f8-888810774a11

📥 Commits

Reviewing files that changed from the base of the PR and between 59ab0db and a77f813.

📒 Files selected for processing (2)
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🧠 Learnings (6)
📓 Common learnings
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 217
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java:107-108
Timestamp: 2026-03-26T05:52:04.269Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`, the `executeProxyRequest()` method unconditionally parses every proxy response body as JSON via `objectMapper.readTree(response.body())`. Therefore, setting `Accept: application/json` on all outbound proxy requests is intentional and consistent — non-JSON responses from Kobo would already cause a Jackson parse exception. The Accept header is forced to `application/json` to encourage Kobo's server to return JSON-formatted error responses (rather than HTML), preventing parse failures on error paths like during sync.
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.
📚 Learning: 2026-03-26T05:52:04.269Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 217
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java:107-108
Timestamp: 2026-03-26T05:52:04.269Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`, the `executeProxyRequest()` method unconditionally parses every proxy response body as JSON via `objectMapper.readTree(response.body())`. Therefore, setting `Accept: application/json` on all outbound proxy requests is intentional and consistent — non-JSON responses from Kobo would already cause a Jackson parse exception. The Accept header is forced to `application/json` to encourage Kobo's server to return JSON-formatted error responses (rather than HTML), preventing parse failures on error paths like during sync.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-26T02:20:22.468Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 198
File: booklore-api/src/main/java/org/booklore/controller/KoboController.java:100-103
Timestamp: 2026-03-26T02:20:22.468Z
Learning: In `booklore-api/src/main/java/org/booklore/controller/KoboController.java`, the Kobo reader device already makes direct outbound connections to Kobo's CDN/servers for other requests (confirmed by imnotjames via testing). Therefore, returning a `307 TEMPORARY_REDIRECT` to `cdn.kobo.com` for non-`BL-` book cover thumbnails (instead of server-side proxying) does NOT introduce a new privacy or behavioral regression — the Kobo device's IP and User-Agent are already exposed to Kobo through other direct requests.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-25T21:02:55.427Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:55.427Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🔇 Additional comments (2)
booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java (1)

107-108: Forced JSON accept/content headers are consistent with proxy behavior.

No issue here — this matches the proxy’s current JSON-only response parsing path and reduces HTML error-body parse failures during sync flows.

Based on learnings: executeProxyRequest() already unconditionally parses proxy responses as JSON, so forcing Accept: application/json is intentional for this path.

booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java (1)

42-65: Entitlement extraction helper is a good resiliency improvement.

Null-safe body handling and controlled fallback on mapping errors are solid for non-fatal sync behavior.

@imnotjames imnotjames force-pushed the fix/216/handle-kobo-proxy-failures branch from a77f813 to a1ab627 Compare March 27, 2026 14:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`:
- Around line 151-153: The upstream sync-token decode is unguarded: wrap the
tokenGenerator.fromBase64(upstreamKoboSyncTokenHeader) call in a try/catch
inside KoboLibrarySyncService so a malformed upstream token does not overwrite
or nullify the existing syncToken; only assign to syncToken if decoding
succeeds, otherwise log a warning including the raw header and error and leave
the current local syncToken unchanged so sync can continue.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53ae274a-7312-4773-97a6-2f83292197dd

📥 Commits

Reviewing files that changed from the base of the PR and between a77f813 and a1ab627.

📒 Files selected for processing (2)
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection
Use MapStruct for entity/DTO mapping

Files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🧠 Learnings (6)
📓 Common learnings
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 217
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java:107-108
Timestamp: 2026-03-26T05:52:04.269Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`, the `executeProxyRequest()` method unconditionally parses every proxy response body as JSON via `objectMapper.readTree(response.body())`. Therefore, setting `Accept: application/json` on all outbound proxy requests is intentional and consistent — non-JSON responses from Kobo would already cause a Jackson parse exception. The Accept header is forced to `application/json` to encourage Kobo's server to return JSON-formatted error responses (rather than HTML), preventing parse failures on error paths like during sync.
📚 Learning: 2026-03-26T05:52:04.269Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 217
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java:107-108
Timestamp: 2026-03-26T05:52:04.269Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java`, the `executeProxyRequest()` method unconditionally parses every proxy response body as JSON via `objectMapper.readTree(response.body())`. Therefore, setting `Accept: application/json` on all outbound proxy requests is intentional and consistent — non-JSON responses from Kobo would already cause a Jackson parse exception. The Accept header is forced to `application/json` to encourage Kobo's server to return JSON-formatted error responses (rather than HTML), preventing parse failures on error paths like during sync.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-26T02:20:22.468Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 198
File: booklore-api/src/main/java/org/booklore/controller/KoboController.java:100-103
Timestamp: 2026-03-26T02:20:22.468Z
Learning: In `booklore-api/src/main/java/org/booklore/controller/KoboController.java`, the Kobo reader device already makes direct outbound connections to Kobo's CDN/servers for other requests (confirmed by imnotjames via testing). Therefore, returning a `307 TEMPORARY_REDIRECT` to `cdn.kobo.com` for non-`BL-` book cover thumbnails (instead of server-side proxying) does NOT introduce a new privacy or behavioral regression — the Kobo device's IP and User-Agent are already exposed to Kobo through other direct requests.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
📚 Learning: 2026-03-25T21:02:55.427Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:55.427Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java
🔇 Additional comments (3)
booklore-api/src/main/java/org/booklore/service/kobo/KoboServerProxy.java (1)

107-108: LGTM: forced JSON Accept is consistent with current proxy behavior.

This change aligns with the proxy’s JSON-only response parsing path and helps avoid HTML error payloads breaking sync flows.

Based on learnings: executeProxyRequest() already unconditionally parses proxy bodies as JSON, so forcing Accept: application/json is intentional to reduce non-JSON error responses.

booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java (2)

42-65: Good resilience improvement in entitlement mapping.

Returning an empty collection on mapping failure keeps sync non-fatal and matches the PR objective for graceful degradation.


138-143: Nice fallback behavior for upstream proxy failures.

Catching proxy exceptions here prevents the sync route from failing hard when Kobo upstream is unavailable.

zachyale pushed a commit to zachyale/grimmory that referenced this pull request Apr 17, 2026
* fix(api): handle kobo proxy sync failures

when the kobo proxy fails, we no longer fail the sync entirely and
instead just skip the kobo request

* fix(api): always `Accept` application/json

* fix(api): handle edge case for an exception being impossibly raised

* fix(api): update try/catch to be just proxy
zachyale pushed a commit to zachyale/grimmory that referenced this pull request Apr 17, 2026
* fix(api): handle kobo proxy sync failures

when the kobo proxy fails, we no longer fail the sync entirely and
instead just skip the kobo request

* fix(api): always `Accept` application/json

* fix(api): handle edge case for an exception being impossibly raised

* fix(api): update try/catch to be just proxy
zachyale pushed a commit that referenced this pull request Apr 17, 2026
* fix(api): handle kobo proxy sync failures

when the kobo proxy fails, we no longer fail the sync entirely and
instead just skip the kobo request

* fix(api): always `Accept` application/json

* fix(api): handle edge case for an exception being impossibly raised

* fix(api): update try/catch to be just proxy
zachyale pushed a commit that referenced this pull request Apr 22, 2026
* fix(api): handle kobo proxy sync failures

when the kobo proxy fails, we no longer fail the sync entirely and
instead just skip the kobo request

* fix(api): always `Accept` application/json

* fix(api): handle edge case for an exception being impossibly raised

* fix(api): update try/catch to be just proxy
@imnotjames imnotjames deleted the fix/216/handle-kobo-proxy-failures branch April 28, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kobo Proxy causes sync errors when the upstream proxy fails

2 participants