@@ -6,6 +6,11 @@ import { getCSECopilotSource } from '@/search/lib/helpers/cse-copilot-docs-versi
66import type { ExtendedRequest } from '@/types'
77import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics'
88
9+ // Maximum time (ms) to wait for the initial response from the upstream
10+ // AI search service. Streaming may take longer once the connection is
11+ // established, but the connect + first-byte must complete within this window.
12+ const AI_SEARCH_TIMEOUT_MS = 4_000
13+
914export const aiSearchProxy = async ( req : ExtendedRequest , res : Response ) => {
1015 const { query, version } = req . body ?? { }
1116
@@ -71,6 +76,7 @@ export const aiSearchProxy = async (req: ExtendedRequest, res: Response) => {
7176 } ,
7277 } ,
7378 {
79+ timeout : AI_SEARCH_TIMEOUT_MS ,
7480 throwHttpErrors : false ,
7581 } ,
7682 )
@@ -143,9 +149,17 @@ export const aiSearchProxy = async (req: ExtendedRequest, res: Response) => {
143149 }
144150 }
145151 } catch ( error ) {
146- statsd . increment ( 'ai-search.route_error' , 1 , diagnosticTags )
147- console . error ( 'Error posting /answers to cse-copilot:' , error )
148- res . status ( 500 ) . json ( { errors : [ { message : 'Internal server error' } ] } )
152+ const isTimeout = error instanceof Error && error . message . includes ( 'timed out' )
153+
154+ if ( isTimeout ) {
155+ statsd . increment ( 'ai-search.timeout' , 1 , diagnosticTags )
156+ console . error ( `AI search request timed out after ${ AI_SEARCH_TIMEOUT_MS } ms` )
157+ res . status ( 504 ) . json ( { errors : [ { message : 'Upstream request timed out' } ] } )
158+ } else {
159+ statsd . increment ( 'ai-search.route_error' , 1 , diagnosticTags )
160+ console . error ( 'Error posting /answers to cse-copilot:' , error )
161+ res . status ( 500 ) . json ( { errors : [ { message : 'Internal server error' } ] } )
162+ }
149163 } finally {
150164 // Ensure reader lock is always released
151165 if ( reader ) {
0 commit comments