{"record":{"id":"7c97a9c1f844b3c8","repo":"thedotmack/claude-mem","slug":"invalid-search-request","errorCode":"INVALID_SEARCH_REQUEST","errorMessage":"Either query or filters required for search","messagePattern":"Either query or filters required for search","errorType":"validation","errorClass":"AppError","httpStatus":400,"severity":"warning","filePath":"src/services/sqlite/SessionSearch.ts","lineNumber":254,"sourceCode":"      case 'relevance':\n        return hasFTS ? `ORDER BY ${ftsTable}.rank ASC` : 'ORDER BY o.created_at_epoch DESC';\n      case 'date_desc':\n        return 'ORDER BY o.created_at_epoch DESC';\n      case 'date_asc':\n        return 'ORDER BY o.created_at_epoch ASC';\n      default:\n        return 'ORDER BY o.created_at_epoch DESC';\n    }\n  }\n\n  searchObservations(query: string | undefined, options: SearchOptions = {}): ObservationSearchResult[] {\n    const params: any[] = [];\n    const { limit = 50, offset = 0, orderBy = 'relevance', ...filters } = options;\n\n    if (!query) {\n      const filterClause = this.buildFilterClause(filters, params, 'o');\n      if (!filterClause) {\n        throw new AppError(SessionSearch.MISSING_SEARCH_INPUT_MESSAGE, 400, 'INVALID_SEARCH_REQUEST');\n      }\n\n      const orderClause = this.buildOrderClause(orderBy, false);\n\n      const sql = `\n        SELECT o.*, o.discovery_tokens\n        FROM observations o\n        WHERE ${filterClause}\n        ${orderClause}\n        LIMIT ? OFFSET ?\n      `;\n\n      params.push(limit, offset);\n      return this.db.prepare(sql).all(...params) as ObservationSearchResult[];\n    }\n\n    if (this._fts5Available) {\n      const filterClause = this.buildFilterClause(filters, params, 'o');","sourceCodeStart":236,"sourceCodeEnd":272,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sqlite/SessionSearch.ts#L236-L272","documentation":"searchObservations() requires either a non-empty query or at least one usable filter. When query is falsy it builds a filter clause from the remaining options; if that clause is empty (no recognized filters contributed), it throws AppError 400 INVALID_SEARCH_REQUEST. This prevents an unbounded SELECT returning the whole observations table.","triggerScenarios":"Calling searchObservations(undefined, {}) or searchObservations('', { limit: 10 }) — i.e. no query and no filters (or only keys buildFilterClause ignores, plus limit/offset/orderBy which are destructured out).","commonSituations":"A caller passes an empty search box with no filters selected; a default invocation from a UI that sends {limit,offset} only; a programmatic caller that conditionally omits both query and filters.","solutions":["Provide a non-empty query string, OR at least one filter the buildFilterClause recognizes (e.g. project, platformSource, dateRange, type).","In calling UI, disable the search action until the user enters a query or picks a filter.","Validate inputs before calling: if neither query nor filters are present, return [] without invoking search."],"exampleFix":"// before\nsearch.searchObservations(undefined, { limit: 50 }); // throws\n\n// after\nif (!query && Object.keys(filters).length === 0) return [];\nsearch.searchObservations(query, { ...filters, limit: 50 });","handlingStrategy":"validation","validationCode":"import type { SearchOptions } from './types.js';\n\nfunction hasSearchInput(query?: string, options: SearchOptions = {}): boolean {\n  if (query && query.trim()) return true;\n  const { limit, offset, orderBy, ...filters } = options;\n  return Object.keys(filters).length > 0;\n}\n\n// usage\nif (!hasSearchInput(query, options)) return [];\nsearch.searchObservations(query, options);","typeGuard":"import { AppError } from '../server/ErrorHandler.js';\n\nfunction isInvalidSearchRequest(e: unknown): boolean {\n  return e instanceof AppError && e.code === 'INVALID_SEARCH_REQUEST';\n}","tryCatchPattern":"try {\n  return search.searchObservations(query, options);\n} catch (e) {\n  if (e instanceof AppError && e.code === 'INVALID_SEARCH_REQUEST') return []; // empty input -> empty result\n  throw e;\n}","preventionTips":["Require a query or at least one filter in the calling UI before issuing search.","Do not pass bare {limit,offset,orderBy} — those are destructured out and do not count as filters.","Share a hasSearchInput helper across all three search entry points."],"tags":["search","validation","sqlite","input-validation","session-search"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}