Skip to content

Weekly defect review: bundle-link race, pagination clamp, SDK case-conversion, MCP doc accuracy - #35

Open
DennisAlund wants to merge 4 commits into
mainfrom
claude/adoring-dirac-nawcvr
Open

Weekly defect review: bundle-link race, pagination clamp, SDK case-conversion, MCP doc accuracy#35
DennisAlund wants to merge 4 commits into
mainfrom
claude/adoring-dirac-nawcvr

Conversation

@DennisAlund

Copy link
Copy Markdown
Member

Weekly defect-hunting review of the app, API, and all three SDKs. Six parallel review passes (backend, frontend, TypeScript SDK, Python SDK, Dart SDK, dependency drift) turned up four fixable defects, all fixed here with regression tests, plus three items left as comments for developer judgment.

Fixed

1. addLinkToBundle returned 500 instead of 404 under a concurrent-delete race

  • What was wrong: BundleRepository.addLink uses INSERT OR IGNORE, which does not suppress foreign-key constraint violations (only UNIQUE/NOT NULL/CHECK). If a bundle or link is deleted between addLinkToBundle's existence checks and the INSERT, the FK constraint on bundle_links trips and D1 throws.
  • Impact: An unhandled 500 instead of the 404 every other concurrent-delete race in this codebase returns (removeSlug, deleteLink, setSlugPrimary, etc. all handle this pattern).
  • Fix: src/services/bundle-management.ts — catch the FK violation and map it to fail(404, ...), mirroring the existing UNIQUE-violation handling in addCustomSlugToLink.
  • Test: src/__tests__/service/bundle-service.test.ts — simulates the race via vi.spyOn(BundleRepository, "addLink").mockRejectedValueOnce(...); fails before the fix (uncaught error), passes after (404).

2. Negative page/per_page on the links listing produced nonsensical pagination

  • What was wrong: parseInt(...) || fallback only catches 0/NaN, not negative integers, in src/index.tsx.
  • Impact: ?page=-3 on a listing with more than a page of links renders an empty table (negative slice bounds) with a nonsensical "-99–-75 of 60" summary; negative per_page produces an inverted slice range.
  • Fix: Clamp both to a minimum of 1 at the parsing site.
  • Test: src/__tests__/page/links-page.test.ts — two new cases (page=-3, per_page=-5) against a 30-link fixture; fail before the fix, pass after.

3. TypeScript SDK silently flattened non-plain-object body values to {}

  • What was wrong: keysToSnake/keysToCamel in sdk/typescript/src/internal/case.ts recursed into anything with typeof value === "object", including Date and other class instances. Object.entries() on a Date returns no own enumerable properties, so any such value passed for a body field was silently flattened to {} on the wire.
  • Impact: A plain-JS caller passing a Date for a field like expiresAt (typed as number in this SDK, but a natural mistake) would send {} instead of a serializable value, with no error.
  • Fix: Restrict recursion to plain objects (Object.prototype or null prototype); class instances now pass through untouched for JSON.stringify to serialize normally.
  • Test: sdk/typescript/tests/client.test.ts — asserts a Date passed for expiresAt serializes to its ISO string, not {}.
  • SDK parity note: TypeScript-only change. Python and Dart don't have an equivalent generic recursive case-conversion helper (Python models use snake_case natively; Dart uses explicit fromJson/toJson), so this bug class doesn't exist in the other two SDKs — confirmed by grep, no parity gap.

4. MCP list_bundles tool description contradicted its actual open-read behavior

  • What was wrong: The tool description said "List bundles owned by the caller," but listBundles() is deliberately open-read across all owners (matching the link/slug model) — the description was simply wrong.
  • Impact: An MCP client (an AI agent) could be misled into treating results as caller-private and relaying another user's bundle data (names, descriptions, click stats) under that false assumption.
  • Fix: src/mcp/server.ts — corrected the description to match list_links' phrasing for the same open-read model. Doc-only, no behavior change.
  • Test: src/__tests__/handler/mcp.test.ts — instantiates ShrtnrMCP, registers tools, and asserts the list_bundles description no longer claims caller-owned scoping.

Test plan

  • Full root suite: 1067 passed (1063 baseline + 4 new)
  • TypeScript SDK suite: 73 passed (72 baseline + 1 new)
  • Python SDK suite: 94 passed, unchanged
  • Each new test independently verified to fail before its fix and pass after

Flagged for developer judgment (not fixed here)

  • Dart SDK — base_client.dart:74,118: response.body.isEmpty (in requestJson) and the entire 2xx path of requestText decode response bytes to a string outside the try/catch that wraps JSON-parse failures into ShrtnrError. A response with a declared UTF-8 charset and invalid byte sequences would throw a raw FormatException, bypassing the SDK's error contract — the same failure class the recent 2xx-JSON-parse fix targeted, one step earlier (byte-decode vs. JSON-syntax). Not fixed here: this sandbox has no Dart toolchain, so I can't compile or run dart test to prove a fix.
  • Python SDK — tests/test_e2e.py:34-38: the module-scoped sync client fixture never closes its httpx.Client, unlike the async_client fixture in the same file which does try/finally cleanup with aclose(). Low-impact (e2e-only, process exits after the run) and only meaningfully testable against a live wrangler dev instance, which isn't available here.
  • src/pages/layout.tsx:79,97,99,122: alt="shrtnr." and alt/title="Oddbit" are hardcoded rather than routed through t(), technically violating the project's i18n rule. Not a functional bug — the brand name is identical across all three locale files — so it's a compliance/style item rather than a defect; left for a judgment call on whether it's worth new translation keys for an unchanging brand string.

Clean

  • Dependency/version drift audit across root package.json and all three SDK manifests: nothing outdated or flagged, matches the recent "upgrade dependencies" PRs.

Generated by Claude Code

claude added 4 commits July 31, 2026 19:32
…t delete

INSERT OR IGNORE into bundle_links does not suppress FK constraint
violations. A bundle or link deleted between addLinkToBundle's existence
checks and the INSERT trips the bundle_links FK and threw an unhandled
D1 error, surfacing as a bare 500 instead of the 404 every other
concurrent-delete race in this codebase returns. Catch the FK violation
and map it to 404, mirroring the existing UNIQUE-violation handling in
addCustomSlugToLink.
parseInt(...) || fallback only catches 0/NaN, not negative integers.
A negative page produced a negative slice start (e.g. page=-3 sliced
sorted.slice(-100, -75), rendering an empty table for links that
exist), and a negative per_page produced an inverted slice range.
Clamp both to a minimum of 1 where they're parsed.
keysToSnake/keysToCamel treated any typeof value === "object" as a
nested object to recurse into, including Date and other class
instances. Object.entries() on a Date returns no own enumerable
properties, so a Date passed for a body field (e.g. expiresAt, despite
its number type) was silently flattened to {} on the wire instead of
being serialized. Restrict recursion to plain objects so class
instances pass through untouched for JSON.stringify to serialize.
listBundles() is deliberately open-read across owners (see the comment
in bundle-management.ts and the "any authenticated caller can add a
link to any bundle" test), matching the link/slug access model. The
list_bundles tool description instead claimed it lists bundles "owned
by the caller," which could mislead an MCP client into treating results
as caller-private and relaying another user's bundle data on that false
assumption. Correct the description to match list_links' phrasing for
the same open-read model.
Copilot AI review requested due to automatic review settings July 31, 2026 19:33
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
shrtnr 8b25f12 Jul 31 2026, 07:34 PM

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes several edge-case defects across the Workers app, MCP tool metadata, and the TypeScript SDK, with targeted regression tests to prevent reintroduction.

Changes:

  • Map a bundle_links foreign-key violation during addLinkToBundle to a 404 to handle concurrent-delete races cleanly.
  • Clamp negative page and per_page query params on the admin links listing to keep pagination consistent.
  • Prevent TypeScript SDK key case-conversion from recursing into non-plain objects (for example Date), avoiding silent {} flattening; add regression coverage.
  • Correct MCP list_bundles tool description to reflect its open-read semantics; add a regression test.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/services/bundle-management.ts Catch FK violations in addLinkToBundle and translate to a 404 to match race-handling patterns.
src/index.tsx Clamp negative pagination query params at parse time.
src/mcp/server.ts Fix list_bundles tool description to match actual visibility semantics.
src/tests/service/bundle-service.test.ts Regression test for the concurrent-delete FK race returning 404.
src/tests/page/links-page.test.ts Regression tests ensuring negative page and per_page are clamped.
src/tests/handler/mcp.test.ts Regression test ensuring list_bundles description does not claim caller-owned scoping.
sdk/typescript/src/internal/case.ts Restrict recursive key transformation to plain objects only.
sdk/typescript/tests/client.test.ts Regression test that Date values do not get flattened to {} during request serialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +663 to +669
const agent = Object.create(ShrtnrMCP.prototype) as ShrtnrMCP;
agent.server = new McpServer({ name: "shrtnr", version: "test" });
await agent.init();
const tools = (agent.server as unknown as {
_registeredTools: Record<string, { description?: string }>;
})._registeredTools;
expect(tools.list_bundles.description).not.toMatch(/owned by the caller/i);
Sign up for free to 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.

3 participants