Weekly defect review: bundle-link race, pagination clamp, SDK case-conversion, MCP doc accuracy - #35
Open
DennisAlund wants to merge 4 commits into
Open
Weekly defect review: bundle-link race, pagination clamp, SDK case-conversion, MCP doc accuracy#35DennisAlund wants to merge 4 commits into
DennisAlund wants to merge 4 commits into
Conversation
…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.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
shrtnr | 8b25f12 | Jul 31 2026, 07:34 PM |
There was a problem hiding this comment.
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_linksforeign-key violation duringaddLinkToBundleto a 404 to handle concurrent-delete races cleanly. - Clamp negative
pageandper_pagequery 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_bundlestool 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
addLinkToBundlereturned 500 instead of 404 under a concurrent-delete raceBundleRepository.addLinkusesINSERT OR IGNORE, which does not suppress foreign-key constraint violations (only UNIQUE/NOT NULL/CHECK). If a bundle or link is deleted betweenaddLinkToBundle's existence checks and the INSERT, the FK constraint onbundle_linkstrips and D1 throws.removeSlug,deleteLink,setSlugPrimary, etc. all handle this pattern).src/services/bundle-management.ts— catch the FK violation and map it tofail(404, ...), mirroring the existing UNIQUE-violation handling inaddCustomSlugToLink.src/__tests__/service/bundle-service.test.ts— simulates the race viavi.spyOn(BundleRepository, "addLink").mockRejectedValueOnce(...); fails before the fix (uncaught error), passes after (404).2. Negative
page/per_pageon the links listing produced nonsensical paginationparseInt(...) || fallbackonly catches0/NaN, not negative integers, insrc/index.tsx.?page=-3on a listing with more than a page of links renders an empty table (negative slice bounds) with a nonsensical "-99–-75 of 60" summary; negativeper_pageproduces an inverted slice range.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
{}keysToSnake/keysToCamelinsdk/typescript/src/internal/case.tsrecursed into anything withtypeof value === "object", includingDateand other class instances.Object.entries()on aDatereturns no own enumerable properties, so any such value passed for a body field was silently flattened to{}on the wire.Datefor a field likeexpiresAt(typed asnumberin this SDK, but a natural mistake) would send{}instead of a serializable value, with no error.Object.prototypeornullprototype); class instances now pass through untouched forJSON.stringifyto serialize normally.sdk/typescript/tests/client.test.ts— asserts aDatepassed forexpiresAtserializes to its ISO string, not{}.fromJson/toJson), so this bug class doesn't exist in the other two SDKs — confirmed by grep, no parity gap.4. MCP
list_bundlestool description contradicted its actual open-read behaviorlistBundles()is deliberately open-read across all owners (matching the link/slug model) — the description was simply wrong.src/mcp/server.ts— corrected the description to matchlist_links' phrasing for the same open-read model. Doc-only, no behavior change.src/__tests__/handler/mcp.test.ts— instantiatesShrtnrMCP, registers tools, and asserts thelist_bundlesdescription no longer claims caller-owned scoping.Test plan
Flagged for developer judgment (not fixed here)
base_client.dart:74,118:response.body.isEmpty(inrequestJson) and the entire 2xx path ofrequestTextdecode response bytes to a string outside the try/catch that wraps JSON-parse failures intoShrtnrError. A response with a declared UTF-8 charset and invalid byte sequences would throw a rawFormatException, 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 rundart testto prove a fix.tests/test_e2e.py:34-38: the module-scoped syncclientfixture never closes itshttpx.Client, unlike theasync_clientfixture in the same file which doestry/finallycleanup withaclose(). Low-impact (e2e-only, process exits after the run) and only meaningfully testable against a livewrangler devinstance, which isn't available here.src/pages/layout.tsx:79,97,99,122:alt="shrtnr."andalt/title="Oddbit"are hardcoded rather than routed throught(), 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
package.jsonand all three SDK manifests: nothing outdated or flagged, matches the recent "upgrade dependencies" PRs.Generated by Claude Code