11import { NextRequest , NextResponse } from "next/server"
22import { executeQuery as executeClickHouseQuery } from "@/lib/clickhouse"
33import { validateRequest } from "@/lib/auth"
4+ import { throwIfAborted , getRequestSignal , handleAbortError } from "@/lib/api-helpers"
45
56/**
67 * Build WHERE clause for domain matching (ClickHouse version)
@@ -56,19 +57,28 @@ function buildKeywordWhereClause(keyword: string, mode: 'domain-only' | 'full-ur
5657}
5758
5859export async function POST ( request : NextRequest ) {
60+ // ✅ Check abort VERY EARLY - before validateRequest
61+ throwIfAborted ( request )
62+
5963 const user = await validateRequest ( request )
6064 if ( ! user ) {
6165 return NextResponse . json ( { success : false , error : "Unauthorized" } , { status : 401 } )
6266 }
6367
6468 try {
69+ // Check if request was aborted early
70+ throwIfAborted ( request )
71+
6572 const body = await request . json ( )
6673 const { targetDomain, timelineGranularity, searchType = 'domain' , type } = body
6774
6875 if ( ! targetDomain || typeof targetDomain !== 'string' ) {
6976 return NextResponse . json ( { error : "targetDomain is required" } , { status : 400 } )
7077 }
7178
79+ // Get signal for passing to database queries
80+ const signal = getRequestSignal ( request )
81+
7282 let whereClause = ''
7383 let params : Record < string , string > = { }
7484
@@ -97,17 +107,28 @@ export async function POST(request: NextRequest) {
97107 // ============================================
98108
99109 if ( type === 'stats' ) {
110+ // Check abort before expensive operations
111+ throwIfAborted ( request )
112+
100113 // Fast data: Subdomains + Paths only
101114 const [ topSubdomains , topPaths ] = await Promise . all ( [
102- getTopSubdomains ( whereClause , params , 10 , searchType , body . keywordMode || 'full-url' , targetDomain ) . catch ( ( e ) => {
103- console . error ( "❌ Subdomains Error:" , e )
115+ getTopSubdomains ( whereClause , params , 10 , searchType , body . keywordMode || 'full-url' , targetDomain , signal ) . catch ( ( e ) => {
116+ // Don't log AbortError as error
117+ if ( e instanceof Error && e . name !== 'AbortError' ) {
118+ console . error ( "❌ Subdomains Error:" , e )
119+ }
104120 return [ ]
105121 } ) ,
106- getTopPaths ( whereClause , params , 10 ) . catch ( ( e ) => {
107- console . error ( "❌ Paths Error:" , e )
122+ getTopPaths ( whereClause , params , 10 , signal ) . catch ( ( e ) => {
123+ if ( e instanceof Error && e . name !== 'AbortError' ) {
124+ console . error ( "❌ Paths Error:" , e )
125+ }
108126 return [ ]
109127 } ) ,
110128 ] )
129+
130+ // Check abort after operations
131+ throwIfAborted ( request )
111132
112133 console . log ( "✅ Stats data retrieved:" , {
113134 topSubdomainsCount : topSubdomains ?. length || 0 ,
@@ -124,11 +145,19 @@ export async function POST(request: NextRequest) {
124145 }
125146
126147 if ( type === 'timeline' ) {
148+ // Check abort before expensive operations
149+ throwIfAborted ( request )
150+
127151 // Slow data: Timeline only
128- const timelineData = await getTimelineData ( whereClause , params , timelineGranularity || 'auto' ) . catch ( ( e ) => {
129- console . error ( "❌ Timeline Error:" , e )
152+ const timelineData = await getTimelineData ( whereClause , params , timelineGranularity || 'auto' , signal ) . catch ( ( e ) => {
153+ if ( e instanceof Error && e . name !== 'AbortError' ) {
154+ console . error ( "❌ Timeline Error:" , e )
155+ }
130156 return [ ]
131157 } )
158+
159+ // Check abort after operations
160+ throwIfAborted ( request )
132161
133162 console . log ( "✅ Timeline data retrieved:" , {
134163 timelineCount : timelineData ?. length || 0 ,
@@ -144,20 +173,32 @@ export async function POST(request: NextRequest) {
144173 }
145174
146175 // Default: Return all data (backward compatible)
176+ // Check abort before expensive operations
177+ throwIfAborted ( request )
178+
147179 const [ timelineData , topSubdomains , topPaths ] = await Promise . all ( [
148- getTimelineData ( whereClause , params , timelineGranularity || 'auto' ) . catch ( ( e ) => {
149- console . error ( "❌ Timeline Error:" , e )
180+ getTimelineData ( whereClause , params , timelineGranularity || 'auto' , signal ) . catch ( ( e ) => {
181+ if ( e instanceof Error && e . name !== 'AbortError' ) {
182+ console . error ( "❌ Timeline Error:" , e )
183+ }
150184 return [ ]
151185 } ) ,
152- getTopSubdomains ( whereClause , params , 10 , searchType , body . keywordMode || 'full-url' , targetDomain ) . catch ( ( e ) => {
153- console . error ( "❌ Subdomains Error:" , e )
186+ getTopSubdomains ( whereClause , params , 10 , searchType , body . keywordMode || 'full-url' , targetDomain , signal ) . catch ( ( e ) => {
187+ if ( e instanceof Error && e . name !== 'AbortError' ) {
188+ console . error ( "❌ Subdomains Error:" , e )
189+ }
154190 return [ ]
155191 } ) ,
156- getTopPaths ( whereClause , params , 10 ) . catch ( ( e ) => {
157- console . error ( "❌ Paths Error:" , e )
192+ getTopPaths ( whereClause , params , 10 , signal ) . catch ( ( e ) => {
193+ if ( e instanceof Error && e . name !== 'AbortError' ) {
194+ console . error ( "❌ Paths Error:" , e )
195+ }
158196 return [ ]
159197 } ) ,
160198 ] )
199+
200+ // Check abort after operations
201+ throwIfAborted ( request )
161202
162203 console . log ( "✅ Overview data retrieved:" , {
163204 timelineCount : timelineData ?. length || 0 ,
@@ -176,12 +217,19 @@ export async function POST(request: NextRequest) {
176217 } )
177218
178219 } catch ( error ) {
220+ // Handle abort errors gracefully
221+ const abortResponse = handleAbortError ( error )
222+ if ( abortResponse ) {
223+ return abortResponse
224+ }
225+
226+ // Handle other errors
179227 console . error ( "❌ Error in overview API:" , error )
180228 return NextResponse . json ( { error : "Failed to get overview data" } , { status : 500 } )
181229 }
182230}
183231
184- async function getTimelineData ( whereClause : string , params : Record < string , string > , granularity : string ) {
232+ async function getTimelineData ( whereClause : string , params : Record < string , string > , granularity : string , signal ?: AbortSignal ) {
185233 // OPTIMIZED DATE PARSING STRATEGY (POST-NORMALIZATION)
186234 // After normalization, log_date is already in standard YYYY-MM-DD format
187235 // Query becomes very simple and fast - directly toDate() without complex parsing
@@ -209,7 +257,8 @@ async function getTimelineData(whereClause: string, params: Record<string, strin
209257 LEFT JOIN devices d ON c.device_id = d.device_id
210258 LEFT JOIN systeminformation si ON d.device_id = si.device_id
211259 ${ whereClause } ` ,
212- params
260+ params ,
261+ signal
213262 ) ) as any [ ]
214263
215264 const range = dateRangeResult [ 0 ]
@@ -254,7 +303,7 @@ async function getTimelineData(whereClause: string, params: Record<string, strin
254303 }
255304
256305 console . log ( "📅 Executing timeline query with granularity:" , actualGranularity )
257- const result = ( await executeClickHouseQuery ( query , params ) ) as any [ ]
306+ const result = ( await executeClickHouseQuery ( query , params , signal ) ) as any [ ]
258307
259308 console . log ( "📊 Timeline query result:" , result . length , "entries" )
260309
@@ -270,7 +319,8 @@ async function getTopSubdomains(
270319 limit : number ,
271320 searchType : string ,
272321 keywordMode : string ,
273- keyword : string
322+ keyword : string ,
323+ signal ?: AbortSignal
274324) {
275325 // SECURITY: Validate limit parameter
276326 const safeLimit = Math . min ( 1000 , Math . max ( 1 , Math . floor ( Number ( limit ) ) || 10 ) )
@@ -296,7 +346,7 @@ async function getTopSubdomains(
296346 FROM credentials c ${ whereClause } GROUP BY full_hostname ORDER BY credential_count DESC LIMIT {queryLimit:UInt32}`
297347 }
298348
299- const result = ( await executeClickHouseQuery ( query , queryParams ) ) as any [ ]
349+ const result = ( await executeClickHouseQuery ( query , queryParams , signal ) ) as any [ ]
300350
301351 // IMPORTANT: Cast count() to Number (ClickHouse returns String)
302352 return result . map ( ( row : any ) => ( {
@@ -305,7 +355,7 @@ async function getTopSubdomains(
305355 } ) )
306356}
307357
308- async function getTopPaths ( whereClause : string , params : Record < string , string > , limit : number ) {
358+ async function getTopPaths ( whereClause : string , params : Record < string , string > , limit : number , signal ?: AbortSignal ) {
309359 // SECURITY: Validate limit parameter
310360 const safeLimit = Math . min ( 1000 , Math . max ( 1 , Math . floor ( Number ( limit ) ) || 10 ) )
311361
@@ -320,7 +370,8 @@ async function getTopPaths(whereClause: string, params: Record<string, string>,
320370 GROUP BY path
321371 ORDER BY credential_count DESC
322372 LIMIT {queryLimit:UInt32}` ,
323- { ...params , queryLimit : safeLimit }
373+ { ...params , queryLimit : safeLimit } ,
374+ signal
324375 ) ) as any [ ]
325376
326377 // IMPORTANT: Cast count() to Number (ClickHouse returns String)
0 commit comments