fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE - #173
fix: fall back to full list endpoints when -lite routes 404 on self-hosted CE#173refract99 wants to merge 2 commits into
Conversation
…osted CE list_projects, list_cycles, list_modules, and get_workspace_members call the *-lite endpoints added in 0.2.10 (projects-lite, cycles-lite, modules-lite, members-lite) for Cloud performance. Self-hosted Plane Community Edition instances don't register these routes, so all four tools 404 even though the workspace, auth, and the non-lite endpoints work fine. Add a lite_or_fallback() helper that retries via the full endpoint on a 404 and reshapes its response into the lite envelope the tool advertises, then wire it into the four affected tools. Fixes makeplane#172, makeplane#170, makeplane#169, makeplane#126 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds a shared 404 fallback utility for lite endpoints, normalizes full responses into lite models, and applies the behavior to cycle, module, project, and workspace member listing tools with unit coverage. ChangesLite listing compatibility
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant ListingTool
participant lite_or_fallback
participant LiteEndpoint
participant FullEndpoint
ListingTool->>lite_or_fallback: call lite listing
lite_or_fallback->>LiteEndpoint: request lite response
LiteEndpoint-->>lite_or_fallback: 404 error
lite_or_fallback->>FullEndpoint: retry full listing
FullEndpoint-->>lite_or_fallback: full response
lite_or_fallback-->>ListingTool: normalized lite response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
plane_mcp/tools/modules.py (1)
62-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueInconsistent params passing:
.model_dump(exclude_none=True)vs direct object.The full-path lambda calls
PaginatedQueryParams(...).model_dump(exclude_none=True)while the lite-path lambda passes theLiteListQueryParamsobject directly. The other tool integrations (cycles, projects) pass params objects directly toclient.*.list(). If the SDK accepts both, this is purely cosmetic; if not, one path may silently drop params. Consider aligning all four tools to use the same style.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane_mcp/tools/modules.py` around lines 62 - 73, Align parameter passing in the lite and full lambdas within the modules listing flow, using the same representation as the corresponding cycles and projects integrations. Update the full-path call in lite_or_fallback and preserve all pagination, ordering, and filtering values without introducing inconsistent serialization.tests/test_lite_fallback.py (1)
45-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider adding tests for
full_callraising and empty results.The suite covers lite success, both fallback shapes, and non-404 re-raise. Two gaps worth adding:
full_call()itself raisesHttpError(e.g., 500) — should propagate, not be swallowed.- Full endpoint returns empty results — should produce an empty lite response without errors.
🧪 Suggested additional tests
def test_propagates_error_from_full_call_on_404(): def lite_call(): raise HttpError("Not Found", status_code=404, response={"error": "Page not found."}) def full_call(): raise HttpError("Server Error", status_code=500, response={"error": "boom"}) with pytest.raises(HttpError) as exc_info: lite_or_fallback(lite_call, full_call, _LiteItem, _LiteResponse) assert exc_info.value.status_code == 500 def test_falls_back_to_empty_bare_list_on_404(): def lite_call(): raise HttpError("Not Found", status_code=404, response={"error": "Page not found."}) result = lite_or_fallback(lite_call, lambda: [], _LiteItem, _LiteResponse) assert isinstance(result, _LiteResponse) assert result.results == [] assert result.total_count == 0🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_lite_fallback.py` around lines 45 - 118, Add tests covering both fallback edge cases in lite_or_fallback: verify an HttpError raised by full_call after a 404 from lite_call propagates with status code 500, and verify an empty full-response list produces an empty _LiteResponse with total_count 0 and no errors.plane_mcp/lite_fallback.py (1)
45-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider using
model_validatefor the bare-list branch for consistency.The paginated branch (line 58) uses
lite_response_cls.model_validate(...), but the bare-list branch constructs the response directly with hardcoded field names. If a lite response model adds a required field or renames one, the direct constructor call breaks whilemodel_validatewould be more forgiving. Usingmodel_validatein both branches would also reduce coupling to specific field names.♻️ Optional refactor for consistency
if isinstance(full, list): - return lite_response_cls( + return lite_response_cls.model_validate( results=results, total_count=len(results), next_cursor="", prev_cursor="", next_page_results=False, prev_page_results=False, count=len(results), total_pages=1, total_results=len(results), )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane_mcp/lite_fallback.py` around lines 45 - 56, Update the bare-list branch in the fallback response flow to build its payload and pass it through lite_response_cls.model_validate, matching the paginated branch’s validation path. Preserve the existing list-derived values and pagination defaults while avoiding direct construction tied to specific response field names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plane_mcp/tools/projects.py`:
- Around line 57-67: Update the fallback parameter construction in list_projects
to preserve include_archived=False from ProjectLiteListQueryParams, using a
compatible params class if PaginatedQueryParams does not support it; also update
get_workspace_members so its fallback MemberQueryParams preserves cursor and
per_page from MemberListQueryParams. Apply the changes in
plane_mcp/tools/projects.py lines 57-67 and plane_mcp/tools/workspaces.py lines
56-73.
---
Nitpick comments:
In `@plane_mcp/lite_fallback.py`:
- Around line 45-56: Update the bare-list branch in the fallback response flow
to build its payload and pass it through lite_response_cls.model_validate,
matching the paginated branch’s validation path. Preserve the existing
list-derived values and pagination defaults while avoiding direct construction
tied to specific response field names.
In `@plane_mcp/tools/modules.py`:
- Around line 62-73: Align parameter passing in the lite and full lambdas within
the modules listing flow, using the same representation as the corresponding
cycles and projects integrations. Update the full-path call in lite_or_fallback
and preserve all pagination, ordering, and filtering values without introducing
inconsistent serialization.
In `@tests/test_lite_fallback.py`:
- Around line 45-118: Add tests covering both fallback edge cases in
lite_or_fallback: verify an HttpError raised by full_call after a 404 from
lite_call propagates with status code 500, and verify an empty full-response
list produces an empty _LiteResponse with total_count 0 and no errors.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 399b53de-4401-43cb-8da6-349ea0298f67
📒 Files selected for processing (6)
plane_mcp/lite_fallback.pyplane_mcp/tools/cycles.pyplane_mcp/tools/modules.pyplane_mcp/tools/projects.pyplane_mcp/tools/workspaces.pytests/test_lite_fallback.py
| params = ProjectLiteListQueryParams(cursor=cursor, per_page=per_page, order_by=order_by, include_archived=False) | ||
|
|
||
| return client.projects.list_lite(workspace_slug=workspace_slug, params=params) | ||
| return lite_or_fallback( | ||
| lambda: client.projects.list_lite(workspace_slug=workspace_slug, params=params), | ||
| lambda: client.projects.list( | ||
| workspace_slug=workspace_slug, | ||
| params=PaginatedQueryParams(cursor=cursor, per_page=per_page, order_by=order_by), | ||
| ), | ||
| ProjectLite, | ||
| PaginatedProjectLiteResponse, | ||
| ) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fallback paths drop query parameters that the lite paths include. Both the list_projects and get_workspace_members fallback lambdas construct params classes that omit filters/pagination present in the lite path, causing behavior inconsistencies for self-hosted users who trigger the 404 fallback.
plane_mcp/tools/projects.py#L57-L67: The lite path setsinclude_archived=FalseviaProjectLiteListQueryParams, but the full path usesPaginatedQueryParamswithout it. If the full/projects/endpoint defaults to including archived projects, the fallback will surface archived projects that lite users never see. Addinclude_archivedto the fallback params (or use a params class that supports it), or confirm the full endpoint defaults to excluding archived projects.plane_mcp/tools/workspaces.py#L56-L73: The lite path passescursorandper_pageviaMemberListQueryParams, but the full path'sMemberQueryParamsomits both. When the fallback triggers, the user's requested page is lost — the full endpoint may return all members or use a default page size. Addcursorandper_pageto the fallbackMemberQueryParamsif it supports them, or use a params class that does.
📍 Affects 2 files
plane_mcp/tools/projects.py#L57-L67(this comment)plane_mcp/tools/workspaces.py#L56-L73
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane_mcp/tools/projects.py` around lines 57 - 67, Update the fallback
parameter construction in list_projects to preserve include_archived=False from
ProjectLiteListQueryParams, using a compatible params class if
PaginatedQueryParams does not support it; also update get_workspace_members so
its fallback MemberQueryParams preserves cursor and per_page from
MemberListQueryParams. Apply the changes in plane_mcp/tools/projects.py lines
57-67 and plane_mcp/tools/workspaces.py lines 56-73.
- projects.py: filter archived projects client-side in the fallback path, since the full /projects/ endpoint's PaginatedQueryParams has no include_archived toggle, matching the lite path's default. - workspaces.py: document why the get_members fallback can't forward cursor/per_page (the endpoint is unpaginated by design). - lite_fallback.py: use model_validate for the bare-list branch, consistent with the paginated branch. - tests: cover full_call raising after a 404 and an empty bare-list fallback. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plane Community Edition does not register the Cloud-only `*-lite` list endpoints; upstream makeplane#162 switched several tools to them, so on a self-hosted CE instance they return HTTP 404 ("Page not found") and the tools are unusable: list_projects, get_workspace_members, get_project_members, list_cycles, list_modules. Add plane_mcp/ce_compat.py with a lite_or_fallback() helper that prefers the Cloud endpoint but, on a 404, transparently retries the full endpoint and reshapes the response into the same paginated envelope the tool already advertises (so the output schema is unchanged). Any other error propagates. - reshape_paginated(): full paginated -> lite envelope (re-validates each row through the trimmed *Lite model; optional keep= filter mirrors the lite endpoint's include_archived=False for projects). - list_to_paginated(): bare member list -> single-page envelope. Works transparently on both Cloud (uses lite) and CE (uses full). Verified against a local CE instance: all five tools now return real data. Refs makeplane#169, makeplane#170, makeplane#172; borrows the approach of upstream PR makeplane#173 and extends it to the two members tools. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Adding another independent confirmation for this PR: I filed #188 for the same root cause (self-hosted Community Edition v1.3.1, The approach in this PR is also the right one rather than just the least surprising. In my issue I initially suggested simply swapping to the non-lite calls — that's wrong, because #162 introduced the lite endpoints deliberately to fix large-workspace timeouts on Cloud. Trying lite first and falling back only on 404 preserves that; a hard swap would regress it. One gap worth folding in: there is a fifth That last one is Note I'm reporting this from source inspection — I haven't run this branch against my instance yet. Happy to test the PR end-to-end against live CE v1.3.1 (including that fifth site if you add it) and report back, if that's useful for getting it reviewed. |
|
Tested this branch ( The fix works. All four covered tools go from 404 to working:
Test suite: Two findings for you. 1. The fifth call site is confirmed live, not just from source
2. The fallback doesn't actually trim to the lite field setThis one only shows up against real data, which is why the green test suite doesn't catch it. Every lite item model in So in results = [lite_item_cls.model_validate(item.model_dump()) for item in items]
That matters beyond cosmetics: #162 introduced the lite endpoints specifically to cut payload on large workspaces, and this quietly gives that back to self-hosted CE users — the exact population this PR is fixing — while the return type still claims it's lite. On a CE workspace with a few hundred projects, The unit tests can't catch it because the fixture's class _LiteItem(BaseModel): # extra defaults to 'ignore'
id: str
name: strso Suggested fix — filter to the declared fields before validating: # Lite models declare extra='allow', so validating a full item would retain every
# extra field and defeat the payload reduction the lite envelope promises.
_lite_fields = set(lite_item_cls.model_fields)
results = [
lite_item_cls.model_validate(
{k: v for k, v in item.model_dump().items() if k in _lite_fields}
)
for item in items
]Verified A/B on the same harness, using the real
All 58 tests still pass with the patch applied, and the four tools still return correctly against the live CE instance. Happy to push either of these as a PR against your branch if that's easier than folding them in yourself. And for whoever reviews: the core 404 fix is sound and confirmed working on real CE hardware — neither finding above is a reason to hold it, they're refinements. |
Summary
list_projects,list_cycles,list_modules, andget_workspace_memberscall the-liteendpoints added in 0.2.10 (projects-lite,cycles-lite,modules-lite,members-lite) for Cloud performance on large workspaces. Self-hosted Plane Community Edition instances don't register these routes, so all four tools returnHTTP 404: Not Found— even though the workspace, auth, and the underlying non-lite endpoints work fine.Reproduced live against a self-hosted CE instance:
retrieve_projectcorrectly reportstotal_modules: 6andtotal_members: 3, whilelist_modulesandget_workspace_membersboth 404 on the same project/workspace.This closely tracks the root cause and suggested fix already described in #172.
Approach
Added
plane_mcp/lite_fallback.py: a smalllite_or_fallback()helper that calls the-liteendpoint first, and on anHTTP 404retries against the equivalent full (non-lite) endpoint, reshaping its response (paginated envelope or bare list, depending on the endpoint) into the lite envelope the tool already advertises in its return type. Non-404 errors are re-raised unchanged.Wired into the four affected tools:
list_projects→ falls back fromprojects.list_lite()toprojects.list()list_cycles→ falls back fromcycles.list_lite()tocycles.list()list_modules→ falls back frommodules.list_lite()tomodules.list()get_workspace_members→ falls back fromworkspaces.get_members_lite()toworkspaces.get_members()This preserves the Cloud perf path (lite endpoints are tried first, so large-workspace timeouts are still avoided where the routes exist) while restoring compatibility with self-hosted CE deployments that predate them.
Test plan
tests/test_lite_fallback.py— 4 unit tests covering: lite success (full_call never invoked), 404 fallback with a paginated full response, 404 fallback with a bare-list full response, and non-404 errors re-raising unchanged.ruff check/ruff format --checkclean on all changed files.pytest tests/ --ignore=tests/test_integration.py(56 passed).list_projects,list_cycles,list_modules,get_workspace_membersall 404'd;list_labels,list_pages,list_milestones,retrieve_work_item_by_identifierwere unaffected).🤖 Generated with Claude Code
Summary by CodeRabbit