{"record":{"id":"8711b3529847f457","repo":"firecrawl/open-lovable","slug":"search-failed","errorCode":null,"errorMessage":"Search failed","messagePattern":"Search failed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"app/api/search/route.ts","lineNumber":29,"sourceCode":"    // Use Firecrawl search to get top 10 results with screenshots\n    const searchResponse = await fetch('https://api.firecrawl.dev/v1/search', {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${process.env.FIRECRAWL_API_KEY}`,\n      },\n      body: JSON.stringify({\n        query,\n        limit: 10,\n        scrapeOptions: {\n          formats: ['markdown', 'screenshot'],\n          onlyMainContent: true,\n        },\n      }),\n    });\n\n    if (!searchResponse.ok) {\n      throw new Error('Search failed');\n    }\n\n    const searchData = await searchResponse.json();\n    \n    // Format results with screenshots and markdown\n    const results = searchData.data?.map((result: any) => ({\n      url: result.url,\n      title: result.title || result.url,\n      description: result.description || '',\n      screenshot: result.screenshot || null,\n      markdown: result.markdown || '',\n    })) || [];\n\n    return NextResponse.json({ results });\n  } catch (error) {\n    console.error('Search error:', error);\n    return NextResponse.json(\n      { error: 'Failed to perform search' },","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/firecrawl/open-lovable/blob/69bd93bae7a9c97ef989eb70aabe6797fb3dac89/app/api/search/route.ts#L11-L47","documentation":"Thrown at app/api/search/route.ts:29 when the raw fetch to https://api.firecrawl.dev/v1/search returns a non-ok HTTP status. Unlike the scrape routes, this handler discards the response body and status, so the real upstream cause (401 bad key, 429 quota, 400 bad query, 5xx outage) is lost and every failure surfaces as the opaque string 'Search failed'. The catch block then converts it into a 500 'Failed to perform search' for the client.","triggerScenarios":"Any POST /api/search where the Firecrawl /v1/search endpoint responds non-2xx: missing/invalid FIRECRAWL_API_KEY, exhausted credits or rate limit, a query payload Firecrawl rejects (e.g. unsupported scrapeOptions combination like markdown+screenshot), or a Firecrawl service outage.","commonSituations":"Environment without FIRECRAWL_API_KEY configured (the header becomes 'Bearer undefined' and Firecrawl returns 401); free-tier quota exhausted after many searches; sending scrapeOptions fields not supported by the deployed Firecrawl version; network/egress restrictions in the hosting environment blocking api.firecrawl.dev.","solutions":["Capture searchResponse.status and body text in the thrown error/log so the true cause is diagnosable — this is the single most important fix.","Verify FIRECRAWL_API_KEY is set in the environment (this route, unlike scrape-website, never checks for it and silently sends an empty credential).","Confirm the /v1/search request payload (limit:10, scrapeOptions formats) against current Firecrawl docs; invalid scrapeOptions can yield 400.","Add retry with backoff for 429/5xx responses and return a mapped status (401->misconfigured, 429->rate limited) instead of a blanket 500.","Set up a Firecrawl status/health check or alerting so outages are recognized quickly."],"exampleFix":"// before\nif (!searchResponse.ok) {\n  throw new Error('Search failed');\n}\n// after\nif (!searchResponse.ok) {\n  const detail = await searchResponse.text();\n  console.error(`Firecrawl search failed (${searchResponse.status}):`, detail);\n  throw new Error(`Search failed (${searchResponse.status}): ${detail.slice(0, 200)}`);\n}","handlingStrategy":"try-catch","validationCode":"const FIRECRAWL_API_KEY = process.env.FIRECRAWL_API_KEY;\nif (!FIRECRAWL_API_KEY) throw new Error('FIRECRAWL_API_KEY is not set');\nif (typeof query !== 'string' || query.trim().length === 0) throw new Error('Query is required');","typeGuard":"function isFirecrawlSearchOk(res: Response) { return res.ok; }","tryCatchPattern":"const res = await fetch('https://api.firecrawl.dev/v1/search', { ... });\nif (!res.ok) {\n  const detail = await res.text().catch(() => '');\n  throw new Error(`Firecrawl search ${res.status}: ${detail.slice(0, 200)}`);\n}\n// in outer catch:\ncatch (e) {\n  console.error('Search error:', e);\n  const status = (e as Error).message.includes('401') ? 503 : 502;\n  return NextResponse.json({ error: 'Search unavailable, try again later' }, { status });\n}","preventionTips":["Never throw opaque messages on non-ok responses — include status code and response body in logs.","Guard for a missing FIRECRAWL_API_KEY before making the call (this route currently omits that check).","Validate the search payload against current Firecrawl /v1/search docs (scrapeOptions support varies by version).","Add backoff/retry on 429 and 5xx; surface a distinct status for auth/quota problems.","Monitor Firecrawl API status and quota usage proactively."],"tags":["firecrawl","search","http-error","network"],"backgroundTag":"upstream-api-http-error","analyzedSha":"69bd93bae7a9c97ef989eb70aabe6797fb3dac89","analyzedAt":"2026-08-28T22:20:32.339Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}