Skip to content

fix(FR-2978): send empty JSON body in vfolder leave_invited to avoid 400#7609

Merged
graphite-app[bot] merged 1 commit into
mainfrom
worktree-fr-2978-fix-leave-shared-folder-400
Jun 9, 2026
Merged

fix(FR-2978): send empty JSON body in vfolder leave_invited to avoid 400#7609
graphite-app[bot] merged 1 commit into
mainfrom
worktree-fr-2978-fix-leave-shared-folder-400

Conversation

@agatha197

@agatha197 agatha197 commented May 27, 2026

Copy link
Copy Markdown
Contributor

Jira: FR-2978

Targets main directly. The previously-stacked generalized fix (#7651, which made newSignedRequest send {} for every null-body non-GET request) was dropped: on legacy backends @check_api_params only falls back to dict(request.query) when the body is empty, so a blanket {} body would suppress that fallback and silently drop query-string params on destroy/restart (e.g. forced, owner_access_key). This PR keeps the fix targeted to leave_invited, the one endpoint whose handler (BodyParam[LeaveVFolderReq]) actually rejects a null body. It also extends the client signature with the optional sharedUserUuid parameter (admin-on-behalf-of).

Summary

Fixes a 400 error when a user tries to leave a shared folder via the Shared Folder Permission modal (both V1 SharedFolderPermissionInfoModal and V2 SharedFolderPermissionInfoModalV2).

Root cause

The Backend.AI manager's POST /folders/{name}/leave REST route parses its body via BodyParam[LeaveVFolderReq]. All fields on LeaveVFolderReq are optional, but the body itself must still be valid JSON.

The frontend client's VFolder.leave_invited was passing null as the body. newSignedRequest serializes null to an empty string '' and sends it with Content-Type: application/json. An empty string is not valid JSON, so the manager rejects the request with 400 before the handler runs.

Both SharedFolderPermissionInfoModal.tsx (V1) and SharedFolderPermissionInfoModalV2.tsx (V2) call the same leave_invited client method, so the bug reproduces in both flows.

Change

The fix is applied in both copies of the API client to keep them consistent:

  • packages/backend.ai-client/src/resources/vfolder.tsload-bearing for the React UI. react/src/global-stores.ts registers globalThis.BackendAIClient = ai.backend.Client from this workspace package. This is what runs in the browser.
  • src/lib/backend.ai-client-node.ts — legacy ESM client; only consumed by src/wsproxy/ (Electron desktop's websocket proxy). Kept in sync to avoid a divergent regression if the Electron side ever calls the leave flow.
  • react/src/hooks/index.tsx — extends the BackendAIClient.vfolder.leave_invited TypeScript signature with the new optional sharedUserUuid parameter (matches LeaveVFolderReq.shared_user_uuid for future admin-on-behalf-of flows).

Both methods now send {} as the JSON body by default. When sharedUserUuid is provided, they send { shared_user_uuid: sharedUserUuid }. No existing call site needs to change for the immediate fix — both modals continue calling leave_invited(folderId) with a single argument.

Test plan

  • As a regular user with an accepted folder invitation, open the data page → click on the shared folder → permission modal → leave button → confirm. Expect a success toast and the folder to disappear from the list (no 400).
  • Repeat for both V1 modal (regular page) and V2 modal (project admin data page).
  • CI: lint / format / relay drift / TypeScript pass.

Verification

bash scripts/verify.sh was run locally. Relay, Lint, Format pass. The TypeScript step reports pre-existing environmental errors (duplicate @types/react between react/ and packages/backend.ai-ui/) that affect files unrelated to this change — CI's clean install is authoritative.

@github-actions github-actions Bot added area:lib Library and SDK related issue. size:S 10~30 LoC labels May 27, 2026

agatha197 commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • flow:merge-queue - adds this PR to the back of the merge queue
  • flow:hotfix - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has required the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for root-coverage

Status Category Percentage Covered / Total
🔵 Lines 8.3% 28 / 337
🔵 Statements 9.14% 32 / 350
🔵 Functions 11.53% 6 / 52
🔵 Branches 9.09% 18 / 198
File CoverageNo changed files found.
Generated in workflow #1733 for commit c590f01 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for react-coverage (./react)

Status Category Percentage Covered / Total
🔵 Lines 6.44% 1798 / 27880
🔵 Statements 5.24% 1994 / 38040
🔵 Functions 5.34% 300 / 5610
🔵 Branches 3.66% 1305 / 35594
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
react/src/hooks/index.tsx 54.74% 39.47% 38.46% 54.47% 28-40, 44-45, 53-65, 72-82, 242-243, 248, 254-261, 353-356, 371-374, 382-403, 411, 421-436, 453, 479-493, 501-530
Generated in workflow #1733 for commit c590f01 by the Vitest Coverage Report Action

@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from f565c6a to 3a3521c Compare May 27, 2026 05:26
@agatha197

Copy link
Copy Markdown
Contributor Author

Thanks for the catch — the workspace package packages/backend.ai-client/src/resources/vfolder.ts is now patched in commit 3a3521c, in addition to the legacy src/lib/backend.ai-client-node.ts. Verified react/src/global-stores.ts:25-27 registers BackendAIClient from the workspace package, so this is the path that actually runs in the browser.

  • Decided to keep both copies in sync to avoid Electron desktop diverging.
  • Kept the optional sharedUserUuid parameter; documented in the PR body that it maps to the manager's LeaveVFolderReq.shared_user_uuid for future admin-on-behalf-of flows. No caller uses it yet — a 1-line bug fix would have been narrower, but the surface is tiny and matches the backend contract.
  • Pre-existing null name guard is unchanged (same shape as delete / info).

PR description updated to reflect the new scope (3 files, both client copies + the TypeScript signature).

@agatha197 agatha197 marked this pull request as ready for review May 27, 2026 05:27
Copilot AI review requested due to automatic review settings May 27, 2026 05:27

Copilot AI 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.

Pull request overview

Fixes a 400 error when leaving a shared (invited) virtual folder by ensuring the POST /folders/{name}/leave request always sends a valid JSON body, aligning with the manager’s BodyParam[LeaveVFolderReq] parsing requirement.

Changes:

  • Update VFolder.leave_invited() (browser client + legacy node client) to send {} instead of null, and optionally include { shared_user_uuid } when provided.
  • Extend the React-side BackendAIClient.vfolder.leave_invited TypeScript signature to accept the new optional sharedUserUuid argument.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/lib/backend.ai-client-node.ts Sends {} (or { shared_user_uuid }) in leave_invited() to avoid invalid empty-string JSON bodies in the legacy node client.
react/src/hooks/index.tsx Updates the BackendAIClient type to include the optional sharedUserUuid parameter for leave_invited().
packages/backend.ai-client/src/resources/vfolder.ts Sends {} (or { shared_user_uuid }) in leave_invited() to avoid 400 responses in the browser client.

@agatha197 agatha197 marked this pull request as draft May 27, 2026 05:31
@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch 2 times, most recently from e47e4d6 to 3a334b0 Compare May 28, 2026 02:57
@agatha197 agatha197 marked this pull request as ready for review May 28, 2026 05:29
@agatha197 agatha197 requested review from ironAiken2 and yomybaby May 28, 2026 05:29
@agatha197 agatha197 marked this pull request as draft May 29, 2026 00:21
@agatha197 agatha197 changed the base branch from main to graphite-base/7609 May 29, 2026 03:50
@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from 3a334b0 to 8408d6f Compare May 29, 2026 03:50
@agatha197 agatha197 changed the base branch from graphite-base/7609 to 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body May 29, 2026 03:50
@agatha197 agatha197 marked this pull request as ready for review May 29, 2026 04:39
@agatha197 agatha197 changed the base branch from 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body to graphite-base/7609 June 2, 2026 06:48
@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from 8408d6f to 4f59d1b Compare June 2, 2026 06:49
@agatha197 agatha197 force-pushed the graphite-base/7609 branch from ed46ce5 to 4e8dcc7 Compare June 2, 2026 06:49
@agatha197 agatha197 changed the base branch from graphite-base/7609 to 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body June 2, 2026 06:49
@agatha197 agatha197 changed the base branch from 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body to graphite-base/7609 June 4, 2026 01:42
@agatha197 agatha197 force-pushed the graphite-base/7609 branch from 4e8dcc7 to 04ccea6 Compare June 4, 2026 01:43
@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from 4f59d1b to 3c01ab8 Compare June 4, 2026 01:43
@agatha197 agatha197 changed the base branch from graphite-base/7609 to 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body June 4, 2026 01:43

@nowgnuesLee nowgnuesLee 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.

LGTM

@agatha197 agatha197 changed the base branch from 05-29-fix_send_empty_json_body_for_non-get_requests_with_null_body to graphite-base/7609 June 8, 2026 11:08
@agatha197 agatha197 force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from 3c01ab8 to f656bc4 Compare June 8, 2026 11:08
@agatha197 agatha197 force-pushed the graphite-base/7609 branch from 04ccea6 to 7f76036 Compare June 8, 2026 11:08
@agatha197 agatha197 changed the base branch from graphite-base/7609 to main June 8, 2026 11:08

@ironAiken2 ironAiken2 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.

LGTM

@graphite-app

graphite-app Bot commented Jun 9, 2026

Copy link
Copy Markdown

Merge activity

…400 (#7609)

**Jira**: [FR-2978](https://lablup.atlassian.net/browse/FR-2978)

> Targets `main` directly. The previously-stacked generalized fix (#7651, which made `newSignedRequest` send `{}` for *every* null-body non-GET request) was dropped: on legacy backends `@check_api_params` only falls back to `dict(request.query)` when the body is empty, so a blanket `{}` body would suppress that fallback and silently drop query-string params on `destroy`/`restart` (e.g. `forced`, `owner_access_key`). This PR keeps the fix targeted to `leave_invited`, the one endpoint whose handler (`BodyParam[LeaveVFolderReq]`) actually rejects a null body. It also extends the client signature with the optional `sharedUserUuid` parameter (admin-on-behalf-of).

## Summary

Fixes a 400 error when a user tries to leave a shared folder via the **Shared Folder Permission** modal (both V1 `SharedFolderPermissionInfoModal` and V2 `SharedFolderPermissionInfoModalV2`).

## Root cause

The Backend.AI manager's `POST /folders/{name}/leave` REST route parses its body via `BodyParam[LeaveVFolderReq]`. All fields on `LeaveVFolderReq` are optional, but the body itself must still be valid JSON.

The frontend client's `VFolder.leave_invited` was passing `null` as the body. `newSignedRequest` serializes `null` to an empty string `''` and sends it with `Content-Type: application/json`. An empty string is not valid JSON, so the manager rejects the request with 400 before the handler runs.

Both `SharedFolderPermissionInfoModal.tsx` (V1) and `SharedFolderPermissionInfoModalV2.tsx` (V2) call the same `leave_invited` client method, so the bug reproduces in both flows.

## Change

The fix is applied in **both** copies of the API client to keep them consistent:

- `packages/backend.ai-client/src/resources/vfolder.ts` — **load-bearing for the React UI**. `react/src/global-stores.ts` registers `globalThis.BackendAIClient = ai.backend.Client` from this workspace package. This is what runs in the browser.
- `src/lib/backend.ai-client-node.ts` — legacy ESM client; only consumed by `src/wsproxy/` (Electron desktop's websocket proxy). Kept in sync to avoid a divergent regression if the Electron side ever calls the leave flow.
- `react/src/hooks/index.tsx` — extends the `BackendAIClient.vfolder.leave_invited` TypeScript signature with the new optional `sharedUserUuid` parameter (matches `LeaveVFolderReq.shared_user_uuid` for future admin-on-behalf-of flows).

Both methods now send `{}` as the JSON body by default. When `sharedUserUuid` is provided, they send `{ shared_user_uuid: sharedUserUuid }`. No existing call site needs to change for the immediate fix — both modals continue calling `leave_invited(folderId)` with a single argument.

## Test plan

- [ ] As a regular user with an accepted folder invitation, open the data page → click on the shared folder → permission modal → leave button → confirm. Expect a success toast and the folder to disappear from the list (no 400).
- [ ] Repeat for both V1 modal (regular page) and V2 modal (project admin data page).
- [ ] CI: lint / format / relay drift / TypeScript pass.

## Verification

`bash scripts/verify.sh` was run locally. Relay, Lint, Format pass. The TypeScript step reports pre-existing environmental errors (duplicate `@types/react` between `react/` and `packages/backend.ai-ui/`) that affect files unrelated to this change — CI's clean install is authoritative.

[FR-2978]: https://lablup.atlassian.net/browse/FR-2978?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
@graphite-app graphite-app Bot force-pushed the worktree-fr-2978-fix-leave-shared-folder-400 branch from f656bc4 to c590f01 Compare June 9, 2026 09:05
@graphite-app graphite-app Bot merged commit c590f01 into main Jun 9, 2026
13 checks passed
@graphite-app graphite-app Bot deleted the worktree-fr-2978-fix-leave-shared-folder-400 branch June 9, 2026 09:06
graphite-app Bot pushed a commit that referenced this pull request Jun 11, 2026
Resolves #7611 (FR-2983)

Stacked on #7609 (FR-2978) — the leave action this test exercises only succeeds with the client fix in that PR; merge bottom-up.

## Summary

Adds an E2E regression test that drives the full share → accept → leave flow against a real Backend.AI cluster. The test reproduces FR-2978 (`POST /folders/{name}/leave` returning 400 because the client sent `null` instead of valid JSON) end-to-end, so the manager↔frontend contract is exercised at the request body level — not just at the TypeScript level — going forward.

## Changes

- `e2e/utils/test-util.ts` — new `leaveSharedFolderAndVerify(page, folderName)` helper. Opens the `SharedFolderPermissionInfoModal` for an invited folder, clicks the Leave button (located by the Tooltip's accessible name *"Leave the shared folder"*), confirms the `Popconfirm`, and asserts both the success toast and the folder's disappearance from the user's Active list.
- `e2e/vfolder/vfolder-crud.spec.ts` — new test *"Invitee can leave a shared vFolder"* extending the existing `VFolder Sharing` block. Reuses `shareVFolderAndVerify` / `acceptAllInvitationAndVerifySpecificFolder`, then drives the new leave helper.
- `e2e/E2E_COVERAGE_REPORT.md` — registers the new flow under VFolder/Data and bumps the route + total counts.

## Local validation

Tried to run the test against the worktree's own Vite dev server, but the host i18n bundle on the dev server in this worktree consistently returns raw keys (`login.E-mailOrUsername` instead of the translated label) — both my new test *and* the existing `User can share vFolder` smoke test fail at the `getByLabel('Email or Username')` step for the same reason. Sibling worktrees' dev servers translate correctly, so the issue is local to this worktree's environment (post FR-2925 TypeScript v6 upgrade), not the test code. CI / the dedicated e2e watchdog runs in a clean environment without this regression.

Once #7609 is merged and the e2e watchdog picks this up, the test should pass; if it doesn't, I will iterate on selectors based on the CI failure report.

## Test plan

- [ ] CI e2e watchdog runs the new test against a real cluster and reports PASS.
- [ ] If FR-2978 is reverted (or the client regresses to sending `null` body), this test fails — confirming it is a real regression guard.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:lib Library and SDK related issue. size:S 10~30 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants