wip on better error handling

This commit is contained in:
bkellam 2025-11-20 16:47:54 -08:00
parent 1d1205f471
commit 5c624cda4f
6 changed files with 61 additions and 30 deletions

View file

@ -48,6 +48,10 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
Sentry.captureException(e); Sentry.captureException(e);
logger.error(e); logger.error(e);
if (e instanceof ServiceErrorException) {
return e.serviceError;
}
if (e instanceof Error) { if (e instanceof Error) {
return unexpectedError(e.message); return unexpectedError(e.message);
} }

View file

@ -287,7 +287,7 @@ export const SearchBar = ({
indentWithTab={false} indentWithTab={false}
autoFocus={autoFocus ?? false} autoFocus={autoFocus ?? false}
/> />
<div className="flex flex-row items-center gap-1"> <div className="flex flex-row items-center gap-1 ml-1">
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span> <span>

View file

@ -33,6 +33,7 @@ import { CodePreviewPanel } from "./codePreviewPanel";
import { FilterPanel } from "./filterPanel"; import { FilterPanel } from "./filterPanel";
import { useFilteredMatches } from "./filterPanel/useFilterMatches"; import { useFilteredMatches } from "./filterPanel/useFilterMatches";
import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel"; import { SearchResultsPanel, SearchResultsPanelHandle } from "./searchResultsPanel";
import { ServiceErrorException } from "@/lib/serviceError";
interface SearchResultsPageProps { interface SearchResultsPageProps {
searchQuery: string; searchQuery: string;
@ -79,7 +80,7 @@ export const SearchResultsPage = ({
useEffect(() => { useEffect(() => {
if (error) { if (error) {
toast({ toast({
description: `❌ Search failed. Reason: ${error.message}`, description: `❌ Search failed. Reason: ${error instanceof ServiceErrorException ? error.serviceError.message : error.message}`,
}); });
} }
}, [error, toast]); }, [error, toast]);
@ -184,7 +185,7 @@ export const SearchResultsPage = ({
<div className="flex flex-col items-center justify-center h-full gap-2"> <div className="flex flex-col items-center justify-center h-full gap-2">
<AlertTriangleIcon className="h-6 w-6" /> <AlertTriangleIcon className="h-6 w-6" />
<p className="font-semibold text-center">Failed to search</p> <p className="font-semibold text-center">Failed to search</p>
<p className="text-sm text-center">{error.message}</p> <p className="text-sm text-center">{error instanceof ServiceErrorException ? error.serviceError.message : error.message}</p>
</div> </div>
) : ( ) : (
<PanelGroup <PanelGroup

View file

@ -1,8 +1,10 @@
'use client'; 'use client';
import { RepositoryInfo, SearchRequest, SearchResultFile, SearchStats, StreamedSearchResponse } from '@/features/search'; import { RepositoryInfo, SearchRequest, SearchResultFile, SearchStats, StreamedSearchResponse } from '@/features/search';
import { useState, useCallback, useRef, useEffect } from 'react'; import { ServiceErrorException } from '@/lib/serviceError';
import { isServiceError } from '@/lib/utils';
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { useCallback, useEffect, useRef, useState } from 'react';
interface CacheEntry { interface CacheEntry {
files: SearchResultFile[]; files: SearchResultFile[];
@ -132,6 +134,14 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
}); });
if (!response.ok) { if (!response.ok) {
// Check if this is a service error response
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
const errorData = await response.json();
if (isServiceError(errorData)) {
throw new ServiceErrorException(errorData);
}
}
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@ -249,7 +259,7 @@ export const useStreamedSearch = ({ query, matches, contextLines, whole, isRegex
...prev, ...prev,
isStreaming: false, isStreaming: false,
timeToSearchCompletionMs, timeToSearchCompletionMs,
error: error as Error, error: error instanceof Error ? error : null,
})); }));
} }
} }

View file

@ -24,6 +24,9 @@ import {
import { parser as _parser } from '@sourcebot/query-language'; import { parser as _parser } from '@sourcebot/query-language';
import { PrismaClient } from '@sourcebot/db'; import { PrismaClient } from '@sourcebot/db';
import { SINGLE_TENANT_ORG_ID } from '@/lib/constants'; import { SINGLE_TENANT_ORG_ID } from '@/lib/constants';
import { ServiceErrorException } from '@/lib/serviceError';
import { StatusCodes } from 'http-status-codes';
import { ErrorCode } from '@/lib/errorCodes';
// Configure the parser to throw errors when encountering invalid syntax. // Configure the parser to throw errors when encountering invalid syntax.
const parser = _parser.configure({ const parser = _parser.configure({
@ -61,6 +64,8 @@ export const parseQuerySyntaxIntoIR = async ({
}, },
prisma: PrismaClient, prisma: PrismaClient,
}): Promise<QueryIR> => { }): Promise<QueryIR> => {
try {
// First parse the query into a Lezer tree. // First parse the query into a Lezer tree.
const tree = parser.parse(query); const tree = parser.parse(query);
@ -90,6 +95,16 @@ export const parseQuerySyntaxIntoIR = async ({
return context.repos.map((repo) => repo.name); return context.repos.map((repo) => repo.name);
}, },
}); });
} catch (error) {
if (error instanceof SyntaxError) {
throw new ServiceErrorException({
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.FAILED_TO_PARSE_QUERY,
message: `Failed to parse query "${query}" with message: ${error.message}`,
});
}
throw error;
}
} }
/** /**

View file

@ -34,4 +34,5 @@ export enum ErrorCode {
API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND', API_KEY_NOT_FOUND = 'API_KEY_NOT_FOUND',
INVALID_API_KEY = 'INVALID_API_KEY', INVALID_API_KEY = 'INVALID_API_KEY',
CHAT_IS_READONLY = 'CHAT_IS_READONLY', CHAT_IS_READONLY = 'CHAT_IS_READONLY',
FAILED_TO_PARSE_QUERY = 'FAILED_TO_PARSE_QUERY',
} }