@@ -11,6 +11,7 @@ import {
1111 AGILOFT_MAX_SEARCH_RECORDS ,
1212 buildNlpSearchBody ,
1313 buildNlpSearchUrl ,
14+ redactAgiloftSecrets ,
1415} from '@/tools/agiloft/utils'
1516import { executeEwRequest , isAgiloftRefusal , readAlrestJson } from '@/tools/agiloft/utils.server'
1617
@@ -53,67 +54,78 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
5354 if ( ! parsed . success ) return parsed . response
5455 const params = parsed . data . body
5556
56- const result = await executeEwRequest < AgiloftNlpSearchResponse > (
57- params ,
58- ( base ) => ( {
59- url : buildNlpSearchUrl ( base ) ,
60- method : 'POST' ,
61- headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
62- body : buildNlpSearchBody ( params ) ,
63- } ) ,
64- async ( response ) => {
65- /**
66- * EWNLPSearch is the one legacy `EW*` operation that answers in the JSON
67- * envelope the `alrest` surface uses — `{success, message, result}` with
68- * `result` as the record array — rather than `EWREST_` assignments. Its
69- * request half is form-encoded like the rest of the `EW*` surface, so
70- * the two halves deliberately use different conventions. Do not
71- * "correct" this to `parseEwRest` to match create.
72- */
73- const payload = await readAlrestJson < Record < string , unknown > [ ] > ( response )
57+ let result : AgiloftNlpSearchResponse
58+ try {
59+ result = await executeEwRequest < AgiloftNlpSearchResponse > (
60+ params ,
61+ ( base ) => ( {
62+ url : buildNlpSearchUrl ( base ) ,
63+ method : 'POST' ,
64+ headers : { 'Content-Type' : 'application/x-www-form-urlencoded' } ,
65+ body : buildNlpSearchBody ( params ) ,
66+ } ) ,
67+ async ( response ) => {
68+ /**
69+ * EWNLPSearch is the one legacy `EW*` operation that answers in the JSON
70+ * envelope the `alrest` surface uses — `{success, message, result}` with
71+ * `result` as the record array — rather than `EWREST_` assignments. Its
72+ * request half is form-encoded like the rest of the `EW*` surface, so
73+ * the two halves deliberately use different conventions. Do not
74+ * "correct" this to `parseEwRest` to match create.
75+ */
76+ const payload = await readAlrestJson < Record < string , unknown > [ ] > ( response )
7477
75- /**
76- * `result` is documented as an array, but a single-record or empty-object
77- * answer would make an unchecked `.slice` throw a TypeError that escapes
78- * as a 500. Normalising keeps an unexpected shape a readable result.
79- */
80- const returned = Array . isArray ( payload ) ? payload : payload ? [ payload ] : [ ]
81- const records = returned . slice ( 0 , AGILOFT_MAX_SEARCH_RECORDS )
78+ /**
79+ * `result` is documented as an array, but a single-record or empty-object
80+ * answer would make an unchecked `.slice` throw a TypeError that escapes
81+ * as a 500. Normalising keeps an unexpected shape a readable result.
82+ */
83+ const returned = Array . isArray ( payload ) ? payload : payload ? [ payload ] : [ ]
84+ const records = returned . slice ( 0 , AGILOFT_MAX_SEARCH_RECORDS )
8285
83- if ( returned . length > records . length ) {
84- logger . warn (
85- `[${ requestId } ] Agiloft NLP search returned ${ returned . length } records; truncated to ${ AGILOFT_MAX_SEARCH_RECORDS } `
86- )
87- }
86+ if ( returned . length > records . length ) {
87+ logger . warn (
88+ `[${ requestId } ] Agiloft NLP search returned ${ returned . length } records; truncated to ${ AGILOFT_MAX_SEARCH_RECORDS } `
89+ )
90+ }
8891
89- return {
90- success : true ,
91- output : {
92- records,
93- totalCount : records . length ,
94- truncated : returned . length > records . length ,
95- } ,
92+ return {
93+ success : true ,
94+ output : {
95+ records,
96+ totalCount : records . length ,
97+ truncated : returned . length > records . length ,
98+ } ,
99+ }
96100 }
101+ )
102+ } catch ( error ) {
103+ /**
104+ * A refusal Agiloft already decided on is a final answer, not a transient
105+ * fault, so it is reported in a 200 body like every sibling operation does.
106+ * A 500 would have the tool runner retry a search Agiloft has already
107+ * declined. The message carries upstream response text and this request's
108+ * credentials travel in its form body, so it is redacted first.
109+ */
110+ if ( isAgiloftRefusal ( error ) ) {
111+ const described = redactAgiloftSecrets ( error . message , params )
112+ logger . warn ( `[${ requestId } ] Agiloft refused the request` , { error : described } )
113+ return NextResponse . json ( {
114+ success : false ,
115+ output : { records : [ ] , totalCount : 0 , truncated : false } ,
116+ error : described ,
117+ } )
97118 }
98- )
99119
100- return NextResponse . json ( result )
101- } catch ( error ) {
102- /**
103- * A refusal Agiloft already decided on is a final answer, not a transient
104- * fault, so it is reported in a 200 body like every sibling operation does.
105- * A 500 would have the tool runner retry a search Agiloft has already
106- * declined.
107- */
108- if ( isAgiloftRefusal ( error ) ) {
109- logger . warn ( `[${ requestId } ] Agiloft refused the request` , { error : error . message } )
110- return NextResponse . json ( {
111- success : false ,
112- output : { records : [ ] , totalCount : 0 , truncated : false } ,
113- error : error . message ,
114- } )
120+ logger . error ( `[${ requestId } ] Error running Agiloft NLP search:` , error )
121+ return NextResponse . json (
122+ { success : false , error : redactAgiloftSecrets ( toError ( error ) . message , params ) } ,
123+ { status : 500 }
124+ )
115125 }
116126
127+ return NextResponse . json ( result )
128+ } catch ( error ) {
117129 logger . error ( `[${ requestId } ] Error running Agiloft NLP search:` , error )
118130
119131 return NextResponse . json ( { success : false , error : toError ( error ) . message } , { status : 500 } )
0 commit comments