Skip to content

fix(web): restore ServiceError boundary in getFileSourceForRepo#1145

Merged
brendan-kellam merged 4 commits intosourcebot-dev:mainfrom
fatmcgav:fix-ai-agent-permissions
Apr 23, 2026
Merged

fix(web): restore ServiceError boundary in getFileSourceForRepo#1145
brendan-kellam merged 4 commits intosourcebot-dev:mainfrom
fatmcgav:fix-ai-agent-permissions

Conversation

@fatmcgav
Copy link
Copy Markdown
Contributor

@fatmcgav fatmcgav commented Apr 23, 2026

Problem

In v4.16.14 (PR #1104, GitLab MR Review Agent), the core file-fetch logic was
extracted from inside sew() into a standalone getFileSourceForRepo function
so it could be called by privileged server-side callers (e.g. the review agent
webhook handler) without going through the auth middleware.

The extraction introduced a missing error boundary. The catch block inside
getFileSourceForRepo handled two known git error patterns and re-threw
everything else
:

// getFileSourceApi.ts — before this fix
} catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    if (errorMessage.includes('does not exist') || ...) return fileNotFound(...);
    if (errorMessage.includes('unknown revision') || ...) return unexpectedError(...);
    throw error;   // ← propagates uncaught; sew() no longer wraps this code path
}

Because getFileSourceForRepo is no longer inside sew(), any re-thrown
exception escapes as a fatal error through the Next.js server task runner,
producing the z.onFatalException / z.attemptTask stack trace seen in
production on v4.16.14.

A second contributing factor: the review agent was also changed in v4.16.14 to
pass ref: pr_payload.head_sha where ref was previously undefined. If the
bare clone hasn't fetched that commit yet, git show throws with
"unknown revision" — which fell into the same unhandled re-throw path.

Rolling back to v4.16.13 restored the original sew()-wrapped code path, which
is why the rollback fixed the issue.

Fix

Two targeted changes in getFileSourceApi.ts:

  1. throw errorreturn unexpectedError(errorMessage) — ensures
    getFileSourceForRepo always returns FileSourceResponse | ServiceError
    and never rejects its promise. This is the root-cause fix.

  2. unexpectedError(...)invalidGitRef(gitRef) for the
    "unknown revision" / "bad revision" / "invalid object name" branch —
    uses the semantically correct error type (already imported), which also
    gives callers a more actionable error when head_sha hasn't been fetched.

Tests

Added getFileSourceApi.test.ts with 19 tests covering:

  • Repository validationNOT_FOUND when repo is absent from the DB;
    correct findFirst query shape.
  • Input validation — path traversal and null-byte paths → FILE_NOT_FOUND;
    refs starting with -INVALID_GIT_REF (flag-injection guard).
  • Git error handling (the regression suite):
    • "does not exist" / "fatal: path"FILE_NOT_FOUND
    • "unknown revision" (unfetched head_sha) → INVALID_GIT_REF
    • "bad revision" / "invalid object name"INVALID_GIT_REF
    • Unrecognised error → UNEXPECTED_ERROR, not a throw — explicit
      regression test; before the fix, .resolves would fail because the
      promise rejected.
  • Successful response — source content, language, ref fallback chain
    (refdefaultBranchHEAD), correct cwd path.
  • Language detection — prefers .gitattributes; falls back to
    filename-based detection when the file is absent.

Files changed

File Change
packages/web/src/features/git/getFileSourceApi.ts throw errorreturn unexpectedError(errorMessage); invalid-ref branch now returns invalidGitRef(gitRef)
packages/web/src/features/git/getFileSourceApi.test.ts New — 19 tests

Fixes #1144

Summary by CodeRabbit

  • Bug Fixes

    • All error paths now return consistent, user-facing service errors (no uncaught exceptions). Unresolved git references produce a clear, specific error and git failures are mapped to appropriate, user-facing responses; function now guarantees a resolved error result.
  • Tests

    • Added comprehensive tests for file-retrieval flows: repo lookup, safety checks, git error mappings, ref handling (ref → defaultBranch → HEAD), and language/source resolution.

 ## Problem

In v4.16.14 (PR sourcebot-dev#1104, GitLab MR Review Agent), the core file-fetch logic was
extracted from inside `sew()` into a standalone `getFileSourceForRepo` function
so it could be called by privileged server-side callers (e.g. the review agent
webhook handler) without going through the auth middleware.

The extraction introduced a missing error boundary. The catch block inside
`getFileSourceForRepo` handled two known git error patterns and **re-threw
everything else**:

```ts
// getFileSourceApi.ts — before this fix
} catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    if (errorMessage.includes('does not exist') || ...) return fileNotFound(...);
    if (errorMessage.includes('unknown revision') || ...) return unexpectedError(...);
    throw error;   // ← propagates uncaught; sew() no longer wraps this code path
}
```

Because `getFileSourceForRepo` is no longer inside `sew()`, any re-thrown
exception escapes as a fatal error through the Next.js server task runner,
producing the `z.onFatalException` / `z.attemptTask` stack trace seen in
production on v4.16.14.

A second contributing factor: the review agent was also changed in v4.16.14 to
pass `ref: pr_payload.head_sha` where `ref` was previously `undefined`. If the
bare clone hasn't fetched that commit yet, `git show` throws with
`"unknown revision"` — which fell into the same unhandled re-throw path.

Rolling back to v4.16.13 restored the original `sew()`-wrapped code path, which
is why the rollback fixed the issue.

 ## Fix

Two targeted changes in `getFileSourceApi.ts`:

1. **`throw error` → `return unexpectedError(errorMessage)`** — ensures
   `getFileSourceForRepo` always returns `FileSourceResponse | ServiceError`
   and never rejects its promise. This is the root-cause fix.

2. **`unexpectedError(...)` → `invalidGitRef(gitRef)`** for the
   `"unknown revision"` / `"bad revision"` / `"invalid object name"` branch —
   uses the semantically correct error type (already imported), which also
   gives callers a more actionable error when `head_sha` hasn't been fetched.

 ## Tests

Added `getFileSourceApi.test.ts` with 19 tests covering:

- **Repository validation** — `NOT_FOUND` when repo is absent from the DB;
  correct `findFirst` query shape.
- **Input validation** — path traversal and null-byte paths → `FILE_NOT_FOUND`;
  refs starting with `-` → `INVALID_GIT_REF` (flag-injection guard).
- **Git error handling** (the regression suite):
  - `"does not exist"` / `"fatal: path"` → `FILE_NOT_FOUND`
  - `"unknown revision"` (unfetched `head_sha`) → `INVALID_GIT_REF`
  - `"bad revision"` / `"invalid object name"` → `INVALID_GIT_REF`
  - **Unrecognised error → `UNEXPECTED_ERROR`, not a throw** — explicit
    regression test; before the fix, `.resolves` would fail because the
    promise rejected.
- **Successful response** — source content, language, ref fallback chain
  (`ref` → `defaultBranch` → `HEAD`), correct `cwd` path.
- **Language detection** — prefers `.gitattributes`; falls back to
  filename-based detection when the file is absent.

 ## Files changed

| File | Change |
|------|--------|
| `packages/web/src/features/git/getFileSourceApi.ts` | `throw error` → `return unexpectedError(errorMessage)`; invalid-ref branch now returns `invalidGitRef(gitRef)` |
| `packages/web/src/features/git/getFileSourceApi.test.ts` | New — 19 tests |
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f315d104-6982-4c43-8fb0-3f76fc03d0e2

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2f003 and 9143d8d.

📒 Files selected for processing (2)
  • packages/web/src/features/git/getFileSourceApi.test.ts
  • packages/web/src/features/git/getFileSourceApi.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/web/src/features/git/getFileSourceApi.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/features/git/getFileSourceApi.ts

Walkthrough

Adds a Vitest test suite for getFileSourceForRepo and updates getFileSourceForRepo to run inside a sew(...) wrapper, map unresolved/bad git refs to a new unresolvedGitRef ServiceError, and ensure all error paths return ServiceError instead of rethrowing exceptions.

Changes

Cohort / File(s) Summary
Test Suite
packages/web/src/features/git/getFileSourceApi.test.ts
New Vitest tests covering repo-not-found, DB errors, unsafe/invalid paths, malicious refs, mapped git errors, successful fetches, language detection (filename and .gitattributes), ref precedence, and working-directory usage.
API: getFileSourceForRepo
packages/web/src/features/git/getFileSourceApi.ts
Wraps logic in sew(async ...); maps git raw errors with messages like "unknown revision", "bad revision", "invalid object name" to unresolvedGitRef(ref); converts other git failures to unexpectedError(...) and avoids rethrowing.
ServiceError helper
packages/web/src/lib/serviceError.ts
Adds unresolvedGitRef(ref: string): ServiceError returning BAD_REQUEST / INVALID_GIT_REF with message Git reference "<ref>" could not be resolved..
Changelog
CHANGELOG.md
Adds Unreleased note describing the fix: wrap getFileSourceForRepo in sew(), ensure error paths return ServiceError, and tighten messaging for unresolved git refs.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as getFileSourceForRepo
  participant DB as PrismaRepo
  participant FS as getRepoPath
  participant Git as simple-git
  participant Attr as .gitattributesResolver
  participant Lang as FilenameLanguageResolver

  Client->>API: Request(repoId, path, ref?)
  API->>DB: findUnique(repoId)
  DB-->>API: repo or null
  alt repo null
    API-->>Client: ServiceError NOT_FOUND
  else
    API->>FS: resolve repo path
    FS-->>API: workingDir
    API->>Git: git.raw(['show', `${ref|defaultBranch|HEAD}:${path}`], {cwd: workingDir})
    Git-->>API: file content or error
    alt file content
      API->>Attr: parse .gitattributes (if present)
      Attr-->>API: language override?
      alt override present
        API-->>Client: ServiceResponse { source, language from .gitattributes }
      else
        API->>Lang: infer from filename
        Lang-->>API: language
        API-->>Client: ServiceResponse { source, language }
      end
    else git error
      alt message matches "unknown/bad/invalid"
        API-->>Client: ServiceError INVALID_GIT_REF (unresolvedGitRef)
      else message indicates missing file/path
        API-->>Client: ServiceError FILE_NOT_FOUND
      else
        API-->>Client: ServiceError UNEXPECTED_ERROR
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(web): restore ServiceError boundary in getFileSourceForRepo' directly and clearly describes the main change—wrapping the function in sew() to properly return ServiceError instead of throwing.
Linked Issues check ✅ Passed The PR fully addresses issue #1144 objectives: wraps getFileSourceForRepo in sew() to prevent uncaught exceptions, returns ServiceError consistently instead of rethrowing, distinguishes unresolved git refs via INVALID_GIT_REF, and includes comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the regression: error handling in getFileSourceApi.ts, new test suite, serviceError helper addition, and changelog entry documenting the fix—no unrelated changes present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/features/git/getFileSourceApi.ts (1)

26-61: ⚠️ Potential issue | 🟠 Major

Restore the error boundary around the whole helper.

Line 61 only converts failures from the first git.raw(['show', ...]) call. prisma.repo.findFirst, getRepoPath, simpleGit().cwd, parsing/language helpers, and URL builders can still throw/reject before or after this block, which still escapes direct callers that only check isServiceError() after await. Wrap the full helper body, while keeping the existing typed git error mapping inside it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/features/git/getFileSourceApi.ts` around lines 26 - 61, The
helper getFileSourceForRepo currently only catches errors from the
git.raw(['show', ...]) call; wrap the entire function body (everything after
entering getFileSourceForRepo) in a top-level try/catch so any rejection from
prisma.repo.findFirst, getRepoPath, simpleGit().cwd, parsing helpers, or URL
builders is converted to a ServiceError via unexpectedError, and preserve the
existing inner mapping for git errors (the current errorMessage checks that
return fileNotFound or invalidGitRef) inside that catch rather than letting
those escape; ensure the function still returns Promise<FileSourceResponse |
ServiceError> and that caught unknown errors are normalized to a string and
passed to unexpectedError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/features/git/getFileSourceApi.test.ts`:
- Line 4: The test is mocking 'simple-git' but creates different mock objects
for the default export and the named export used in the test; update the vi.mock
call to provide an explicit factory that maps both the default export and the
named export to the same mock function so the SUT (which imports default
simpleGit) receives the configured mock chain—create a single mock function and
return it as both default and simpleGit in the factory so tests that import
either { simpleGit } or default simpleGit use the identical mock.

In `@packages/web/src/features/git/getFileSourceApi.ts`:
- Around line 58-59: The getFileSourceApi branch currently returns
invalidGitRef(gitRef) for errors like "unknown revision"/"bad revision"/"invalid
object name", which produces a syntactic-validation-style message; change this
to produce an "unresolved-ref" message while preserving errorCode:
INVALID_GIT_REF by either (a) adding a new helper (e.g.,
unresolvedGitRef(gitRef)) that formats user text about an unavailable/unresolved
ref and use that in getFileSourceApi, or (b) extend invalidGitRef to accept a
flag/variant for unresolved vs syntactic errors and call it with the unresolved
variant; update the service error helper(s) in serviceError.ts (where
invalidGitRef is defined) accordingly and ensure callers still get errorCode:
INVALID_GIT_REF.

---

Outside diff comments:
In `@packages/web/src/features/git/getFileSourceApi.ts`:
- Around line 26-61: The helper getFileSourceForRepo currently only catches
errors from the git.raw(['show', ...]) call; wrap the entire function body
(everything after entering getFileSourceForRepo) in a top-level try/catch so any
rejection from prisma.repo.findFirst, getRepoPath, simpleGit().cwd, parsing
helpers, or URL builders is converted to a ServiceError via unexpectedError, and
preserve the existing inner mapping for git errors (the current errorMessage
checks that return fileNotFound or invalidGitRef) inside that catch rather than
letting those escape; ensure the function still returns
Promise<FileSourceResponse | ServiceError> and that caught unknown errors are
normalized to a string and passed to unexpectedError.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a07528a5-c91d-4aee-82e5-a9184471b2e1

📥 Commits

Reviewing files that changed from the base of the PR and between d111397 and 41f978f.

📒 Files selected for processing (2)
  • packages/web/src/features/git/getFileSourceApi.test.ts
  • packages/web/src/features/git/getFileSourceApi.ts

Comment thread packages/web/src/features/git/getFileSourceApi.test.ts Outdated
Comment thread packages/web/src/features/git/getFileSourceApi.ts Outdated
Three fixes to getFileSourceForRepo, which was extracted outside sew()
in v4.16.14 and lost its error boundary:

- Wrap the entire function body in a top-level try/catch so exceptions
  from prisma, getRepoPath, simpleGit().cwd(), language helpers, and URL
  builders are converted to unexpectedError rather than propagating as
  fatal Next.js task-runner exceptions.

- Add unresolvedGitRef() to serviceError.ts (errorCode: INVALID_GIT_REF,
  distinct message) and use it for "unknown revision"/"bad revision"/
  "invalid object name" git errors, replacing the syntactic invalidGitRef
  message that was misleading for unfetched head_sha refs.

- Fix the simple-git vi.mock() factory in the test file to map both the
  default and named exports to the same hoisted mock fn, ensuring the SUT
  and the test body reference identical mocks. Add a test for the outer
  catch (DB throws) and tighten INVALID_GIT_REF assertions to distinguish
  syntactic from unresolved-ref errors by message content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread packages/web/src/features/git/getFileSourceApi.ts Outdated
Gavin Williams and others added 2 commits April 23, 2026 18:34
…rRepo

The top-level try/catch added to getFileSourceForRepo silently swallowed
unexpected errors with no Sentry capture or log entry. Replace it with
sew(), which provides the same ServiceError conversion plus Sentry
reporting and structured logging.

The inner git-specific catch is preserved unchanged — sew() only handles
anything that escapes it (DB errors, getRepoPath, URL builders, etc.).

Update the sew mock in the test file to catch and convert exceptions,
matching the real sew() behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@brendan-kellam brendan-kellam left a comment

Choose a reason for hiding this comment

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

LGTM thanks!

The review agent is a unique path where we don't actually have a auth context to lean on (and therefore, cannot use withAuth).

Some loose thoughts: Eventually, I would like to get to the point where there is some amount of formalization between the "API layer" where calls are authenticated and authorized for the caller, and the "service layer" where the business logic lives. The idea would then be that we could better reason about what code does / does not contain auth guarantees.

For the most part, this formalization is not necessary right now, but it's something to consider

@brendan-kellam brendan-kellam merged commit 651700b into sourcebot-dev:main Apr 23, 2026
7 checks passed
@fatmcgav fatmcgav deleted the fix-ai-agent-permissions branch April 23, 2026 18:30
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.

[bug] v4.16.14 results in EACCESS permission denied errors

2 participants