Skip to content

Conversation

Ravishekhar7870
Copy link
Contributor

@Ravishekhar7870 Ravishekhar7870 commented Oct 13, 2025

Summary by CodeRabbit

  • New Features

    • You can now cancel individual file uploads directly from the chat box (click the X while uploading).
    • Updated status text and tooltips to indicate uploads are cancelable.
    • Automatic cleanup of in-progress uploads when leaving the page.
  • Bug Fixes

    • Cancelled uploads no longer trigger error toasts.
    • Server responds appropriately to cancelled uploads, avoiding misleading failure states.
  • Tests

    • Added comprehensive tests covering upload cancellation, cleanup, and progress state updates.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

Walkthrough

Adds abortable file uploads across frontend and server: introduces AbortController-based per-file cancellation in ChatBox, augments upload progress Zustand store with removeProgress, adds tests for cancellation flows, updates server to return 499 on aborted attachments, and adds the zustand dependency.

Changes

Cohort / File(s) Summary
Dependency addition
frontend/package.json
Adds dependency zustand@^5.0.8.
ChatBox abortable uploads
frontend/src/components/ChatBox.tsx
Implements per-file AbortController management, attaches signals to fetch, aborts on remove/unmount, updates UI state with uploadAborted, and handles AbortError without toasts.
Component tests for cancellation
frontend/src/components/__tests__/ChatBox.test.tsx
Adds tests covering abortable uploads, multi-controller cleanup, and AbortError handling during upload.
Upload progress store + tests
frontend/src/store/useUploadProgressStore.ts, frontend/src/store/__tests__/useUploadProgressStore.test.ts
Adds removeProgress(uploadId) to store; tests initialization, cancelation, status updates, and controller cleanup.
Server AbortError handling
server/api/files.ts
Detects AbortError during attachment upload and returns HTTP 499 with “Upload cancelled”; preserves existing error handling otherwise.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant C as ChatBox (React)
  participant S as UploadStore (Zustand)
  participant B as fetch()/AbortController
  participant API as Server /files
  participant T as Toast

  U->>C: Select file(s) to upload
  C->>S: set uploading state per file
  C->>B: Create AbortController per file
  C->>API: POST /files (signal=controller.signal)
  alt Upload completes
    API-->>C: 200 OK
    C->>S: mark uploaded
  else User cancels
    U-->>C: Click X (Cancel)
    C->>B: controller.abort()
    API-->>C: 499 Upload cancelled
    C->>S: mark uploadAborted & cleanup
    C--x T: (no error toast on AbortError)
  end
Loading
sequenceDiagram
  autonumber
  participant C as ChatBox (Unmount)
  participant S as UploadStore
  participant B as AbortController Map

  C->>B: Iterate controllers
  loop each in-flight upload
    C->>B: controller.abort()
  end
  C->>B: clear()
  C->>S: removeProgress / cleanup state
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • zereraz
  • shivamashtikar
  • junaid-shirur
  • devesh-juspay

Poem

I thump my paw—abort!—then cheer,
Tiny bytes hop out of here.
X marks cancel, swift and kind,
499 says “never mind.”
Previews fade, controllers rest,
My burrow’s code is clean and blessed.
🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “Bugfix cmgoz1sdq00009mh3txngg3fm” is generic and does not summarize the main changes in this pull request, which implements cancelable file uploads with AbortController across the frontend and backend, adds related tests, and updates the upload progress store. As a result, it fails to convey the intent or scope of the feature to reviewers. Please update the pull request title to clearly describe the primary change, for example “Enable cancelable file uploads with AbortController and handle cancellations with HTTP 499”.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-cmgoz1sdq00009mh3txngg3fm

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Ravishekhar7870, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the file upload experience by introducing the capability to cancel ongoing uploads. It integrates AbortController to efficiently terminate network requests and provides clear visual feedback to the user. The changes also include comprehensive error handling for aborted uploads across both the client and server, leading to a more stable and user-friendly system.

Highlights

  • File Upload Cancellation: Implemented the ability to cancel ongoing file uploads using the AbortController API, providing users with more control over their interactions.
  • Enhanced User Interface: Updated the file preview 'X' button to visually indicate when it will cancel an upload (turning red) and added a hint to the 'Uploading files...' message, improving user feedback.
  • Robust Error Handling: Introduced specific error handling for AbortError on both the frontend (preventing unnecessary error toasts) and backend (logging the cancellation and returning a 499 status code), ensuring graceful management of cancelled requests.
  • New Dependency & Test Coverage: Added the zustand state management library and introduced new unit tests for the ChatBox upload cancellation logic and the useUploadProgressStore to ensure reliability.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a valuable feature: the ability to cancel file uploads in the chat box. The implementation correctly uses AbortController on the frontend and handles AbortError on the backend. The UI is also updated to reflect the cancellation action, which is great for user experience.

I've identified a high-severity bug related to state management where the count of uploading files is decremented twice upon cancellation, which could lead to UI inconsistencies. I've also provided feedback on the new test file for ChatBox, suggesting improvements to make the tests more robust by testing the component's behavior directly rather than just the underlying concepts.

}
return prev.filter((f) => f.id !== id)
})
setUploadingFilesCount((prev) => Math.max(0, prev - 1))
Copy link
Contributor

Choose a reason for hiding this comment

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

high

There's a bug here where uploadingFilesCount is decremented twice for a cancelled upload. It's decremented here, and also in the finally block of the uploadFiles function (line 914), which will execute after the upload promise is rejected due to the abort signal.

This can lead to an incorrect count and potential UI bugs. The finally block is a more reliable place to handle the decrement as it covers success, failure, and abort cases. I suggest removing this line to fix the double-decrement issue.

Comment on lines +29 to +106
describe('ChatBox Upload Cancellation', () => {
let mockAuthFetch: any

beforeEach(() => {
vi.clearAllMocks()
mockAuthFetch = vi.fn()
vi.doMock('@/utils/authFetch', () => ({
authFetch: mockAuthFetch
}))
})

it('should handle upload cancellation correctly', async () => {
// Create a mock AbortController
const abortController = new AbortController()

// Mock a file upload that can be aborted
const uploadPromise = new Promise((resolve, reject) => {
abortController.signal.addEventListener('abort', () => {
const error = new Error('Upload aborted')
error.name = 'AbortError'
reject(error)
})
})

mockAuthFetch.mockReturnValue(uploadPromise)

// Test that aborting the upload triggers the correct behavior
act(() => {
abortController.abort()
})

// Verify the upload was cancelled
await expect(uploadPromise).rejects.toThrow('Upload aborted')
})

it('should clean up upload controllers on component unmount', () => {
// This test would require more complex setup with actual component rendering
// For now, we'll just verify the concept
const controllers = new Map<string, AbortController>()
const controller1 = new AbortController()
const controller2 = new AbortController()

controllers.set('file1', controller1)
controllers.set('file2', controller2)

// Simulate cleanup
controllers.forEach((controller) => {
controller.abort()
})
controllers.clear()

expect(controllers.size).toBe(0)
expect(controller1.signal.aborted).toBe(true)
expect(controller2.signal.aborted).toBe(true)
})

it('should handle AbortError gracefully in upload completion', async () => {
const abortError = new Error('Request aborted')
abortError.name = 'AbortError'

// Mock the upload function to handle AbortError
const mockUploadFile = async (file: File, signal: AbortSignal) => {
if (signal.aborted) {
throw abortError
}
return { fileId: 'test-id', fileName: file.name }
}

const testFile = new File(['test'], 'test.txt', { type: 'text/plain' })
const abortController = new AbortController()

// Abort before upload
abortController.abort()

// Verify AbortError is thrown
await expect(mockUploadFile(testFile, abortController.signal)).rejects.toThrow('Request aborted')
})
}) No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

These tests verify the concepts of upload cancellation (like how AbortController works) but they don't actually test the ChatBox component's implementation. The component itself is imported but never rendered or tested.

To make these tests more effective and ensure the cancellation feature works as expected from a user's perspective, I recommend using @testing-library/react to render the ChatBox component, simulate file selection, trigger the cancellation by clicking the 'X' button, and then assert that the upload was aborted and the UI was updated correctly.

Here's an example of how such a test could be structured:

import { render, screen, fireEvent, waitFor } from '@testing-library/react';

it('should cancel an ongoing upload when the cancel button is clicked', async () => {
  // 1. Mock authFetch to simulate a pending upload that can be aborted
  const abortSpy = vi.fn();
  mockAuthFetch.mockImplementation((url, options) => {
    options.signal.addEventListener('abort', () => {
      abortSpy();
      const error = new Error('Upload aborted');
      error.name = 'AbortError';
      // This would normally be handled by fetch, but for test we reject a promise
    });
    // Return a promise that never resolves to simulate a long upload
    return new Promise(() => {});
  });

  // 2. Render the ChatBox component with necessary props
  render(<ChatBox {...mockProps} />);

  // 3. Simulate file selection
  const fileInput = screen.getByTitle(/Attach files/); // Use a more accessible selector
  const file = new File(['content'], 'test.png', { type: 'image/png' });
  await act(async () => {
    fireEvent.change(fileInput, { target: { files: [file] } });
  });

  // 4. Find and click the cancel button
  const cancelButton = await screen.findByTitle('Cancel upload');
  fireEvent.click(cancelButton);

  // 5. Assert that the abort function was called and the file is removed from UI
  await waitFor(() => {
    expect(abortSpy).toHaveBeenCalled();
    expect(screen.queryByText('test.png')).not.toBeInTheDocument();
  });
});

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
frontend/src/store/useUploadProgressStore.ts (1)

48-49: removeProgress addition looks good

API surface and behavior align with cancel/finish patterns.

You could dedupe finishUpload/cancelUpload/removeProgress via a small helper (set to null by id) to avoid repetition.

Also applies to: 145-149

server/api/files.ts (1)

401-407: Correct: return 499 on aborted uploads

Graceful AbortError handling with 499 is appropriate and matches client cancellation semantics.

  • Broaden detection to include DOMException-based aborts:
const isAbort = (e: unknown) => typeof e === 'object' && e !== null && 'name' in (e as any) && (e as any).name === 'AbortError'
if (isAbort(error)) {
  loggerWithChild({ email }).info("Attachment upload was cancelled by client")
  throw new HTTPException(499, { message: "Upload cancelled" })
}

Also consider adding the same 499-on-abort handling in handleFileUpload for consistency across endpoints.

frontend/package.json (1)

67-69: Zustand addition aligns with new store

Dependency looks correct for the introduced store patterns.

  • Optional: align vitest packages to latest 3.2.x when convenient and verify @testing-library peer deps as per their latest guidance. Based on learnings
frontend/src/components/ChatBox.tsx (1)

847-851: Abortable upload flow: solid; a couple of minor polish ideas

Per-file AbortController usage, signal wiring, UI affordances, and unmount cleanup look good.

  • Use a tiny isAbortError helper for consistency:
const isAbortError = (e: unknown) => typeof e === 'object' && e !== null && (e as any).name === 'AbortError'

Then if (isAbortError(error)) { /* ... */ }

  • Consider marking the file as uploading: false immediately upon abort before removal to avoid transient spinner flicker.

Also applies to: 859-859, 884-895, 912-914, 2258-2263, 2911-2917, 2971-2977, 2989-2989

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 959357a and 0777098.

⛔ Files ignored due to path filters (1)
  • frontend/bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (6)
  • frontend/package.json (1 hunks)
  • frontend/src/components/ChatBox.tsx (11 hunks)
  • frontend/src/components/__tests__/ChatBox.test.tsx (1 hunks)
  • frontend/src/store/__tests__/useUploadProgressStore.test.ts (1 hunks)
  • frontend/src/store/useUploadProgressStore.ts (2 hunks)
  • server/api/files.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/store/__tests__/useUploadProgressStore.test.ts (1)
frontend/src/store/useUploadProgressStore.ts (1)
  • useUploadProgress (52-155)
🪛 GitHub Actions: TypeScript Build Check
frontend/src/store/__tests__/useUploadProgressStore.test.ts

[error] 1-1: bunx tsc -b: TS6133: 'vi' is declared but its value is never read.

frontend/src/components/__tests__/ChatBox.test.tsx

[error] 1-1: bunx tsc -b: TS6133: 'renderHook' is declared but its value is never read.


[error] 3-3: bunx tsc -b: TS6133: 'ChatBox' is declared but its value is never read.

Comment on lines +1 to +3
import { renderHook, act } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ChatBox } from '../ChatBox'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix TS6133: remove unused imports

Build fails due to unused renderHook and ChatBox.

Apply:

-import { renderHook, act } from '@testing-library/react'
+import { act } from '@testing-library/react'
-import { ChatBox } from '../ChatBox'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { renderHook, act } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ChatBox } from '../ChatBox'
import { act } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
🧰 Tools
🪛 GitHub Actions: TypeScript Build Check

[error] 1-1: bunx tsc -b: TS6133: 'renderHook' is declared but its value is never read.


[error] 3-3: bunx tsc -b: TS6133: 'ChatBox' is declared but its value is never read.

🤖 Prompt for AI Agents
In frontend/src/components/__tests__/ChatBox.test.tsx around lines 1-3, the test
file imports unused symbols causing TS6133; remove unused imports by deleting
renderHook from the first import and removing the ChatBox import (or import only
needed symbols) so the file only imports what it actually uses (e.g., keep act
and vitest imports) and update any references accordingly.

Comment on lines +1005 to +1022
// If the file is currently uploading, abort the upload
if (fileToRemove?.uploading) {
const controller = uploadControllers.current.get(id)
if (controller) {
controller.abort()
uploadControllers.current.delete(id)
}

// Remove from UI immediately for aborted uploads
setSelectedFiles((prev) => {
if (fileToRemove?.preview) {
URL.revokeObjectURL(fileToRemove.preview)
}
return prev.filter((f) => f.id !== id)
})
setUploadingFilesCount((prev) => Math.max(0, prev - 1))
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: double-decrement of uploadingFilesCount on abort

When canceling an in-flight upload, removeFile decrements the counter, and the upload promise finally decrements again. This can take the count negative and prematurely resolve send gating.

Fix by removing the early decrement and relying on the promise finally:

       if (controller) {
         controller.abort()
         uploadControllers.current.delete(id)
       }
       
       // Remove from UI immediately for aborted uploads
       setSelectedFiles((prev) => {
         if (fileToRemove?.preview) {
           URL.revokeObjectURL(fileToRemove.preview)
         }
         return prev.filter((f) => f.id !== id)
       })
-      setUploadingFilesCount((prev) => Math.max(0, prev - 1))
       return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If the file is currently uploading, abort the upload
if (fileToRemove?.uploading) {
const controller = uploadControllers.current.get(id)
if (controller) {
controller.abort()
uploadControllers.current.delete(id)
}
// Remove from UI immediately for aborted uploads
setSelectedFiles((prev) => {
if (fileToRemove?.preview) {
URL.revokeObjectURL(fileToRemove.preview)
}
return prev.filter((f) => f.id !== id)
})
setUploadingFilesCount((prev) => Math.max(0, prev - 1))
return
}
// If the file is currently uploading, abort the upload
if (fileToRemove?.uploading) {
const controller = uploadControllers.current.get(id)
if (controller) {
controller.abort()
uploadControllers.current.delete(id)
}
// Remove from UI immediately for aborted uploads
setSelectedFiles((prev) => {
if (fileToRemove?.preview) {
URL.revokeObjectURL(fileToRemove.preview)
}
return prev.filter((f) => f.id !== id)
})
return
}
🤖 Prompt for AI Agents
In frontend/src/components/ChatBox.tsx around lines 1005-1022, the abort branch
of removeFile calls setUploadingFilesCount decrement immediately and the upload
promise's finally also decrements, causing double-decrement and possible
negative counts; remove the early setUploadingFilesCount((prev) => Math.max(0,
prev - 1)) from this abort branch (still revoke preview URL, remove file from
UI, abort and delete the controller) and rely on the upload promise's finally to
decrement the uploadingFilesCount so the counter stays consistent.

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix TS6133: remove unused vi import

Build fails due to unused import.

Apply:

-import { describe, it, expect, beforeEach, vi } from 'vitest'
+import { describe, it, expect, beforeEach } from 'vitest'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { describe, it, expect, beforeEach } from 'vitest'
🧰 Tools
🪛 GitHub Actions: TypeScript Build Check

[error] 1-1: bunx tsc -b: TS6133: 'vi' is declared but its value is never read.

🤖 Prompt for AI Agents
In frontend/src/store/__tests__/useUploadProgressStore.test.ts around line 1,
the import list includes an unused `vi` from 'vitest' causing TS6133; remove
`vi` from the import statement (or replace the import with only the used
symbols) so the file imports only describe, it, expect, beforeEach to eliminate
the unused-import TypeScript error.

@Ravishekhar7870 Ravishekhar7870 deleted the bugfix-cmgoz1sdq00009mh3txngg3fm branch October 15, 2025 06:05
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.

1 participant