Skip to content

OUT-3598: surface Dropbox error_summary on content-endpoint failures#99

Merged
SandipBajracharya merged 2 commits into
mainfrom
OUT-3598
May 4, 2026
Merged

OUT-3598: surface Dropbox error_summary on content-endpoint failures#99
SandipBajracharya merged 2 commits into
mainfrom
OUT-3598

Conversation

@SandipBajracharya
Copy link
Copy Markdown
Collaborator

@SandipBajracharya SandipBajracharya commented Apr 30, 2026

Summary

  • Read the JSON error body in DropboxClient._downloadFile / _uploadFile and forward Dropbox's actual error_summary / error shape, so 409s show the real reason (path/not_found, path/restricted_content, etc.) in Sentry instead of the opaque "Response failed with a 4xx code"
  • Replace deep node_modules/copilot-node-sdk/dist/codegen import with a structural isCopilotApiError guard colocated in CopilotAPI.ts
  • Defensive optional chaining on a pre-existing error.error.error.path['.tag'] access in createAndUploadFileInDropbox's catch (was a latent TypeError when 409s with non-matching body shapes propagated)

No behavior change for callers, retries, or downstream tasks. Orphan fileFolderSync rows continue flowing through the existing resync path.

Linear: OUT-3598
Sentry: DROPBOX-INTEGRATION-17

Test plan

  • pnpm typecheck passes
  • pnpm lint passes
  • After merge, monitor DROPBOX-INTEGRATION-17 — next occurrence should show real Dropbox error_summary (e.g. path/not_found/...) in the event payload
  • Confirm Folder already exists retry path in createAndUploadFileToAssembly still triggers (existing behavior, just swapped from instanceof to structural guard)

🤖 Generated with Claude Code

…ures

The 409 from `_downloadFile` was opaque in Sentry because the manual fetch
path threw `DropboxResponseError` with a hand-rolled `error_summary` and
discarded the response body. Read the JSON body and forward Dropbox's actual
`error_summary` / `error` shape so future failures show which 409 it is
(`path/not_found`, `path/restricted_content`, `unsupported_file`, etc.).
Adjacent cleanups uncovered during review:
- replace deep `node_modules/copilot-node-sdk/dist/codegen` import with a
  structural `isCopilotApiError` guard in `CopilotAPI.ts`
- defensive optional chaining on `error.error.error.path['.tag']` in
  `createAndUploadFileInDropbox`'s catch (the catch wraps two call sites
  with different 409 body shapes; non-matching ones used to TypeError).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Apr 30, 2026

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dropbox-integration Ready Ready Preview, Comment Apr 30, 2026 0:44am

Request Review

@SandipBajracharya SandipBajracharya changed the title fix(OUT-3598): surface Dropbox error_summary on content-endpoint failures OUT-3598: surface Dropbox error_summary on content-endpoint failures Apr 30, 2026
@SandipBajracharya SandipBajracharya requested review from arpandhakal and priosshrsth and removed request for arpandhakal April 30, 2026 11:38
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR surfaces real Dropbox error_summary values on content-endpoint failures by parsing the JSON response body in buildDropboxResponseError, replaces a fragile deep node_modules import with a structural isCopilotApiError guard, and fixes a latent TypeError in createAndUploadFileInDropbox by splitting the try/catch so that only filesGetMetadata is caught — preventing a path/not_found from _uploadFile from being silently misrouted to the "create" branch. Both previously flagged concerns are addressed: the try/catch split resolves the not_found routing hazard, and statusText in the structural guard correctly excludes DropboxResponseError which does not carry that field.

Confidence Score: 5/5

Safe to merge — no P0 or P1 findings; previously flagged concerns are resolved by the refactoring.

All three files show targeted, well-reasoned changes. The try/catch split directly fixes the misrouting hazard raised in the prior review thread. Optional chaining on error.error eliminates the latent TypeError. buildDropboxResponseError is properly guarded against body-read failures and malformed JSON. No new defects introduced.

No files require special attention.

Important Files Changed

Filename Overview
src/lib/dropbox/DropboxClient.ts Adds buildDropboxResponseError to read the JSON body from content-endpoint failures and construct a DropboxResponseError with the real error_summary/error payload; applies it in _downloadFile and _uploadFile. Logic is sound: body read is guarded with .catch(() => ''), JSON parse is in a try/catch, and the fallback wraps the raw text.
src/lib/copilot/CopilotAPI.ts Introduces isCopilotApiError structural type guard and CopilotApiError type to replace the deep node_modules import. Guard checks url + status + statusText + body together; statusText is intentionally used to distinguish from DropboxResponseError which doesn't carry that field.
src/features/sync/lib/Sync.service.ts Splits the monolithic try/catch in createAndUploadFileInDropbox so only filesGetMetadata is guarded — 409 not_found from _uploadFile can no longer be misrouted to the "create" branch. Adds optional chaining on the error.error body access to fix the latent TypeError. Swaps instanceof CopilotApiError for isCopilotApiError in createAndUploadFileToAssembly.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant Sync as SyncService
    participant DBX as DropboxClient (SDK)
    participant Content as DropboxClient (Content API)
    participant Dropbox

    Caller->>Sync: createAndUploadFileInDropbox(file, dbxFilePath, fileType)

    Note over Sync,DBX: Isolated try/catch — only filesGetMetadata
    Sync->>DBX: filesGetMetadata(dbxFilePath)
    alt 200 OK
        DBX-->>Sync: existing metadata
        alt .tag === FOLDER
            Sync-->>Caller: { dbxFileId }
        else .tag === FILE
            Sync->>DBX: filesMoveV2(old → timestamped)
            Sync->>Content: _uploadFile(file, dbxFilePath)
            Content->>Dropbox: POST /files/upload (stream)
            alt 200 OK
                Dropbox-->>Content: FileMetadata JSON
                Content-->>Sync: DropboxFileMetadata
                Sync-->>Caller: { dbxFileId, contentHash }
            else 4xx error
                Content->>Content: buildDropboxResponseError(response)
                Note over Content: reads JSON body → DropboxResponseError with real error_summary
                Content-->>Sync: throws DropboxResponseError
                Sync-->>Caller: propagates (uncaught)
            end
        end
    else 409 not_found
        Note over Sync: swallow — existing stays undefined
        alt fileType === FOLDER
            Sync->>DBX: filesCreateFolderV2(dbxFilePath)
            DBX-->>Sync: folderMetadata
            Sync-->>Caller: { dbxFileId }
        else fileType === FILE
            Sync->>Content: _uploadFile(file, dbxFilePath)
            Content->>Dropbox: POST /files/upload (stream)
            Content-->>Sync: DropboxFileMetadata or throws
            Sync-->>Caller: { dbxFileId, contentHash } or propagates
        end
    else other error
        DBX-->>Sync: throws
        Sync-->>Caller: propagates
    end
Loading

Reviews (2): Last reviewed commit: "fix(OUT-3598): narrow not_found catch an..." | Re-trigger Greptile

Comment thread src/features/sync/lib/Sync.service.ts Outdated
Comment thread src/lib/copilot/CopilotAPI.ts
Two follow-ups from review feedback on 0426a12:

- `createAndUploadFileInDropbox` previously wrapped both `filesGetMetadata`
  and the rename-then-reupload path in a single try/catch keyed on a
  409/`path/not_found` body. Now that `_uploadFile` reproduces that same
  canonical error shape, a post-rename upload failing because the parent
  folder is missing would have been mis-routed into the "doesn't exist,
  create it" branch and double-fired `uploadFileInDropbox`. Narrow the
  try/catch to wrap only `filesGetMetadata` and lift the
  folder-exists / file-rename / create-new branches into flat conditional
  flow on the captured metadata result.

- Tighten `isCopilotApiError`: also require `url: string`. Pairing it with
  the existing `statusText` + `body` checks pins the guard to the SDK's
  `ApiError` shape (`url`, `status`, `statusText`, `body`, `request`) so it
  can't accidentally match unrelated Error subclasses that happen to carry
  `status` (DropboxResponseError, our own APIError, future SDK wrappers).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@SandipBajracharya
Copy link
Copy Markdown
Collaborator Author

@greptileai

Copy link
Copy Markdown
Collaborator

@priosshrsth priosshrsth left a comment

Choose a reason for hiding this comment

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

lgtm

@SandipBajracharya SandipBajracharya merged commit 274ecb0 into main May 4, 2026
5 checks passed
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.

2 participants