Weekly defect review: case-insensitive QR slugs, concurrent-delete races, SDK JSON-parse bug#31
Open
DennisAlund wants to merge 5 commits into
Open
Weekly defect review: case-insensitive QR slugs, concurrent-delete races, SDK JSON-parse bug#31DennisAlund wants to merge 5 commits into
DennisAlund wants to merge 5 commits into
Conversation
Slugs are always stored lowercase (link-management.ts normalizes on write), and every other lookup path lowercases before comparing. handleLinkQr and the get_link_qr MCP tool compared the raw ?slug= value with ===, so an uppercase/mixed-case slug that passes the route's own permissive regex schema returned a spurious 404 even though the link and slug both exist.
setSlugPrimary re-read the link with a non-null assertion after calling SlugRepository.setPrimary. If the link was deleted between the initial membership check and that final read, getById legitimately returns null and the assertion forced it into ok(null), producing an HTTP 200 with a null body instead of a 404. Same TOCTOU class already fixed for bundle update/archive/unarchive/delete in 551d205. Regression test in link-service.test.ts uses vi.spyOn on LinkRepository.getById to simulate the race.
…delete getBundleAnalytics checked BundleRepository.getById itself, then force-unwrapped ClickRepository.getBundleStats with a non-null assertion. getBundleStats runs its own independent getById internally, so a delete landing between the two reads makes it legitimately return null, and the assertion turned that into ok(null): an HTTP 200 with a null body instead of a 404. Same TOCTOU class already fixed for update/archive/unarchive/delete in 551d205; this analytics path was missed. Regression test in bundle-service.test.ts mocks ClickRepository.getBundleStats to simulate the race.
HttpClient.request's error branch already wraps res.json() in
try/catch, but the success branch called it bare. A 2xx response with
a non-JSON body (HTML error page from an upstream proxy, a truncated
response) threw a raw SyntaxError instead of the documented
ShrtnrError contract, so a caller doing
`catch (err) { if (err instanceof ShrtnrError) ... }` would not catch
it. Same bug class just fixed in the Python SDK (537a790).
Regression test in client.test.ts mocks a text/html 2xx body.
requestJson called jsonDecode(response.body) bare on the 2xx success path while the error path already guards its own jsonDecode call. A non-JSON 2xx body (HTML error page, truncated response) threw a raw FormatException instead of the documented ShrtnrError contract. Same bug class just fixed in the Python SDK (537a790) and the TypeScript SDK. Regression test in client_test.dart mocks a text/html 2xx body. Dart is not available in this environment, so the test could not be executed locally; it mirrors the exact fixture pattern of the existing "throws ShrtnrError on 4xx" test in the same group, and the fix was traced manually against jsonDecode's documented FormatException throw.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
shrtnr | 8d6860e | Jul 10 2026, 07:25 PM |
There was a problem hiding this comment.
Pull request overview
This PR addresses a set of small but user-visible defects across the REST API, MCP server, and TypeScript/Dart SDKs—primarily around slug case handling, concurrent-delete race behavior returning 200/null, and consistent SDK error wrapping when a 2xx response body is not valid JSON.
Changes:
- Make QR slug selection case-insensitive in both the REST QR handler and the MCP
get_link_qrtool. - Fix concurrent-delete race outcomes by returning
404(instead of200with anullpayload) insetSlugPrimaryandgetBundleAnalytics. - Wrap JSON parsing failures on 2xx responses in SDK-specific
ShrtnrError(TypeScript + Dart) with regression tests.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/link-management.ts | Returns a proper 404 when a link disappears before the post-update re-read in setSlugPrimary. |
| src/services/bundle-management.ts | Returns a proper 404 when analytics stats resolve to null due to a concurrent bundle deletion. |
| src/mcp/server.ts | Lowercases the requested slug when resolving the target slug for get_link_qr. |
| src/api/qr.ts | Lowercases the ?slug= query parameter before matching against stored slugs. |
| src/tests/service/link-service.test.ts | Adds regression coverage for setSlugPrimary concurrent-delete returning 404. |
| src/tests/service/bundle-service.test.ts | Adds regression coverage for getBundleAnalytics returning 404 when stats are null. |
| src/tests/handler/mcp.test.ts | Adds MCP regression test proving get_link_qr slug matching is case-insensitive. |
| src/tests/handler/api-links.test.ts | Adds REST regression test proving GET .../qr?slug= matching is case-insensitive. |
| sdk/typescript/tests/client.test.ts | Adds test ensuring non-JSON 2xx bodies throw ShrtnrError rather than raw SyntaxError. |
| sdk/typescript/src/internal/http.ts | Wraps res.json() on success path with try/catch and throws ShrtnrError on parse failure. |
| sdk/dart/test/client_test.dart | Adds test ensuring non-JSON 2xx bodies throw ShrtnrError rather than raw FormatException. |
| sdk/dart/lib/src/base_client.dart | Wraps jsonDecode on success path with try/catch and throws ShrtnrError on parse failure. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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 across the app, API, and all three SDKs. Five fixes, each with a regression test that fails before and passes after.
Fixes
1. QR slug lookup was case-sensitive (
src/api/qr.ts,src/mcp/server.ts)link-management.tsnormalizes on write), and every other lookup path lowercases before comparing, buthandleLinkQrand theget_link_qrMCP tool compared the raw?slug=/slugargument with===.[a-zA-Z0-9_-]+schema) got a spurious 404 for a slug that demonstrably exists on the link.api-links.test.tsandmcp.test.tseach add a case-mismatch regression test.2.
setSlugPrimaryreturned 200/null instead of 404 on a concurrent link delete (src/services/link-management.ts)SlugRepository.setPrimaryno-ops silently if the link/slug is gone; the service then force-unwrapped the trailinggetByIdre-read with!.nullbody instead of a 404. Same TOCTOU class already fixed for bundle update/archive/unarchive/delete in a prior commit (551d205); this path was missed.fail(404, "Link not found").link-service.test.ts,vi.spyOn(LinkRepository, "getById")simulates the race.3.
getBundleAnalyticsreturned 200/null instead of 404 on a concurrent bundle delete (src/services/bundle-management.ts)getByIdcheck passes, thenClickRepository.getBundleStatsruns its own independentgetByIdinternally and can legitimately returnnullif the bundle is deleted in between; the result was force-unwrapped with!.nullbody instead of 404, onGET /bundles/:id/analyticsand the equivalent MCP tool.fail(404, "Bundle not found").bundle-service.test.ts, mocksClickRepository.getBundleStatsto simulate the race.4/5. TS and Dart SDKs threw a raw parse exception instead of
ShrtnrErroron a non-JSON 2xx bodyHttpClient.request(TS,sdk/typescript/src/internal/http.ts) andrequestJson(Dart,sdk/dart/lib/src/base_client.dart) wrapres.json()/jsonDecodein try/catch on the error path but called it bare on the success path.SyntaxError/FormatException, breaking the documentedcatch (err) { if (err instanceof ShrtnrError) }contract. Same bug class already fixed in the Python SDK (537a790); TS and Dart had it too.ShrtnrError(status, "Invalid JSON response: ..."), mirroring the Python fix exactly.client.test.ts(TS) andclient_test.dart(Dart) each mock atext/html2xx body.sdk/dart/test/client_test.dartcould not be run locally. The fix was traced manually againstjsonDecode's documentedFormatExceptionthrow, and the test mirrors the exact fixture pattern of the adjacent "throws ShrtnrError on 4xx" test in the same group.Test suite
72 test files / 1051 testspassed.68 testspassed.Flagged for developer judgment (not fixed, no code changes)
sdk/dart/README.md): quickstart examples passrange: '7d'/accent: 'green'as raw strings where the real methods require theTimelineRange/BundleAccentenums, so copy-pasted examples fail to compile. Theupdate()examples document anupdate(id, {field: value})shape but the real signature isupdate(Link)/update(Bundle)(whole-object). Exported type namesDateClickCount/SlugClickCountdon't match the realDateCount/SlugCount. All four are docs-only; no regression test is possible for markdown examples, so left for a developer to fix the README text._base.py'sparse_json_responsefix (537a790) is exercised only by sync tests intest_client.py;test_async_client.pyhas no equivalent non-JSON-2xx orqr(size=...)test, even though both paths share the same underlying code. Not a live bug today, but a future divergence of the async path would go uncaught.typescriptis one major behind (6.x → 7.0.2) in both the root app and TS SDK;@cloudflare/workers-typesis one major behind (4.x → 5.x).yarn auditreports 19 (root) / 7 (TS SDK) vulnerabilities, all in transitive dev/test tooling (vite/esbuildviavitest) or bundled into@modelcontextprotocol/sdk's Express transport (fast-uri,ip-address,qs) — none critical, none with a drop-in safe patch identified. Worth a planned major-version bump pass, not an urgent patch.🤖 Generated with Claude Code
Generated by Claude Code