Skip to content

Commit 9143d8d

Browse files
Gavin Williamsclaude
andcommitted
refactor(web): use sew() instead of bare try/catch in getFileSourceForRepo
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>
1 parent 2c2f003 commit 9143d8d

2 files changed

Lines changed: 73 additions & 69 deletions

File tree

packages/web/src/features/git/getFileSourceApi.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@ vi.mock('next/headers', () => ({
4444
headers: vi.fn().mockResolvedValue(new Headers()),
4545
}));
4646
vi.mock('@/middleware/sew', () => ({
47-
sew: async <T>(fn: () => Promise<T> | T): Promise<T> => fn(),
47+
sew: async <T>(fn: () => Promise<T> | T): Promise<T> => {
48+
try {
49+
return await fn();
50+
} catch (error) {
51+
return {
52+
errorCode: 'UNEXPECTED_ERROR',
53+
message: error instanceof Error ? error.message : String(error),
54+
} as T;
55+
}
56+
},
4857
}));
4958
vi.mock('@/middleware/withAuth', () => ({
5059
withOptionalAuth: vi.fn(),
@@ -117,7 +126,7 @@ describe('getFileSourceForRepo', () => {
117126
});
118127
});
119128

120-
it('returns UNEXPECTED_ERROR when the database throws (outer catch)', async () => {
129+
it('returns UNEXPECTED_ERROR when the database throws (caught by sew)', async () => {
121130
mockFindFirst.mockRejectedValue(new Error('DB connection refused'));
122131

123132
const result = await getFileSourceForRepo(

packages/web/src/features/git/getFileSourceApi.ts

Lines changed: 62 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -26,84 +26,79 @@ export type FileSourceResponse = z.infer<typeof fileSourceResponseSchema>;
2626
export const getFileSourceForRepo = async (
2727
{ path: filePath, repo: repoName, ref }: FileSourceRequest,
2828
{ org, prisma }: { org: Org; prisma: PrismaClient },
29-
): Promise<FileSourceResponse | ServiceError> => {
30-
try {
31-
const repo = await prisma.repo.findFirst({
32-
where: { name: repoName, orgId: org.id },
33-
});
34-
if (!repo) {
35-
return notFound(`Repository "${repoName}" not found.`);
36-
}
29+
): Promise<FileSourceResponse | ServiceError> => sew(async () => {
30+
const repo = await prisma.repo.findFirst({
31+
where: { name: repoName, orgId: org.id },
32+
});
33+
if (!repo) {
34+
return notFound(`Repository "${repoName}" not found.`);
35+
}
3736

38-
if (!isPathValid(filePath)) {
39-
return fileNotFound(filePath, repoName);
40-
}
37+
if (!isPathValid(filePath)) {
38+
return fileNotFound(filePath, repoName);
39+
}
4140

42-
if (ref !== undefined && !isGitRefValid(ref)) {
43-
return invalidGitRef(ref);
44-
}
41+
if (ref !== undefined && !isGitRefValid(ref)) {
42+
return invalidGitRef(ref);
43+
}
4544

46-
const { path: repoPath } = getRepoPath(repo);
47-
const git = simpleGit().cwd(repoPath);
45+
const { path: repoPath } = getRepoPath(repo);
46+
const git = simpleGit().cwd(repoPath);
4847

49-
const gitRef = ref ?? repo.defaultBranch ?? 'HEAD';
48+
const gitRef = ref ?? repo.defaultBranch ?? 'HEAD';
5049

51-
let fileContent: string;
52-
try {
53-
fileContent = await git.raw(['show', `${gitRef}:${filePath}`]);
54-
} catch (error: unknown) {
55-
const errorMessage = error instanceof Error ? error.message : String(error);
56-
if (errorMessage.includes('does not exist') || errorMessage.includes('fatal: path')) {
57-
return fileNotFound(filePath, repoName);
58-
}
59-
if (errorMessage.includes('unknown revision') || errorMessage.includes('bad revision') || errorMessage.includes('invalid object name')) {
60-
return unresolvedGitRef(gitRef);
61-
}
62-
return unexpectedError(errorMessage);
50+
let fileContent: string;
51+
try {
52+
fileContent = await git.raw(['show', `${gitRef}:${filePath}`]);
53+
} catch (error: unknown) {
54+
const errorMessage = error instanceof Error ? error.message : String(error);
55+
if (errorMessage.includes('does not exist') || errorMessage.includes('fatal: path')) {
56+
return fileNotFound(filePath, repoName);
6357
}
64-
65-
let gitattributesContent: string | undefined;
66-
try {
67-
gitattributesContent = await git.raw(['show', `${gitRef}:.gitattributes`]);
68-
} catch {
69-
// No .gitattributes in this repo/ref, that's fine
58+
if (errorMessage.includes('unknown revision') || errorMessage.includes('bad revision') || errorMessage.includes('invalid object name')) {
59+
return unresolvedGitRef(gitRef);
7060
}
61+
return unexpectedError(errorMessage);
62+
}
63+
64+
let gitattributesContent: string | undefined;
65+
try {
66+
gitattributesContent = await git.raw(['show', `${gitRef}:.gitattributes`]);
67+
} catch {
68+
// No .gitattributes in this repo/ref, that's fine
69+
}
7170

72-
const language = gitattributesContent
73-
? (resolveLanguageFromGitAttributes(filePath, parseGitAttributes(gitattributesContent)) ?? detectLanguageFromFilename(filePath))
74-
: detectLanguageFromFilename(filePath);
71+
const language = gitattributesContent
72+
? (resolveLanguageFromGitAttributes(filePath, parseGitAttributes(gitattributesContent)) ?? detectLanguageFromFilename(filePath))
73+
: detectLanguageFromFilename(filePath);
7574

76-
const externalWebUrl = getCodeHostBrowseFileAtBranchUrl({
77-
webUrl: repo.webUrl,
78-
codeHostType: repo.external_codeHostType,
79-
branchName: gitRef,
80-
filePath,
81-
});
75+
const externalWebUrl = getCodeHostBrowseFileAtBranchUrl({
76+
webUrl: repo.webUrl,
77+
codeHostType: repo.external_codeHostType,
78+
branchName: gitRef,
79+
filePath,
80+
});
8281

83-
const baseUrl = env.AUTH_URL;
84-
const webUrl = `${baseUrl}${getBrowsePath({
85-
repoName: repo.name,
86-
revisionName: ref,
87-
path: filePath,
88-
pathType: 'blob',
89-
})}`;
82+
const baseUrl = env.AUTH_URL;
83+
const webUrl = `${baseUrl}${getBrowsePath({
84+
repoName: repo.name,
85+
revisionName: ref,
86+
path: filePath,
87+
pathType: 'blob',
88+
})}`;
9089

91-
return {
92-
source: fileContent,
93-
language,
94-
path: filePath,
95-
repo: repoName,
96-
repoCodeHostType: repo.external_codeHostType,
97-
repoDisplayName: repo.displayName ?? undefined,
98-
repoExternalWebUrl: repo.webUrl ?? undefined,
99-
webUrl,
100-
externalWebUrl,
101-
} satisfies FileSourceResponse;
102-
} catch (error: unknown) {
103-
const errorMessage = error instanceof Error ? error.message : String(error);
104-
return unexpectedError(errorMessage);
105-
}
106-
};
90+
return {
91+
source: fileContent,
92+
language,
93+
path: filePath,
94+
repo: repoName,
95+
repoCodeHostType: repo.external_codeHostType,
96+
repoDisplayName: repo.displayName ?? undefined,
97+
repoExternalWebUrl: repo.webUrl ?? undefined,
98+
webUrl,
99+
externalWebUrl,
100+
} satisfies FileSourceResponse;
101+
});
107102

108103
export const getFileSource = async ({ path: filePath, repo: repoName, ref }: FileSourceRequest, { source }: { source?: string } = {}): Promise<FileSourceResponse | ServiceError> => sew(() => withOptionalAuth(async ({ org, prisma, user }) => {
109104
if (user) {

0 commit comments

Comments
 (0)