@@ -7,6 +7,9 @@ import { Job, JsonValue, UploadedFile } from '@/lib/types';
77
88// POST /api/proxy - Proxy request to deAPI
99export async function POST ( request : Request ) {
10+ // Track a persisted job so the outer catch can mark it failed instead of
11+ // leaving a pre-created 'sending' stub stuck forever on an unexpected error.
12+ let persistedJobId : string | undefined ;
1013 try {
1114 const contentType = request . headers . get ( 'content-type' ) || '' ;
1215 let endpointId : string ;
@@ -187,8 +190,20 @@ export async function POST(request: Request) {
187190 } ) ;
188191 }
189192
190- const priceData = await priceResponse . json ( ) ;
191- console . log ( '[deapi-tester] Price response:' , priceResponse . status , priceData ) ;
193+ // Parse defensively — a non-JSON price response (e.g. HTML error page)
194+ // should not crash the whole request, just skip the estimate.
195+ const priceText = await priceResponse . text ( ) ;
196+ let priceData ;
197+ try {
198+ priceData = JSON . parse ( priceText ) ;
199+ } catch {
200+ priceData = null ;
201+ }
202+ console . log (
203+ '[deapi-tester] Price response:' ,
204+ priceResponse . status ,
205+ priceData ?? priceText . slice ( 0 , 300 )
206+ ) ;
192207 // Most price endpoints return { data: { price } }; prompt-enhancement
193208 // returns a top-level { price }.
194209 const price = priceData ?. data ?. price ?? priceData ?. price ;
@@ -214,7 +229,9 @@ export async function POST(request: Request) {
214229
215230 // Create or reuse the job entry before making the request. When the client
216231 // pre-created a 'sending' stub (providedJobId), update it in place so the row
217- // transitions sending -> pending without duplicating.
232+ // transitions sending -> pending without duplicating. Price-only checks are a
233+ // throwaway pre-calculation — there is no request to restore/duplicate to —
234+ // so they are NOT persisted to history (all job writes are skipped below).
218235 const jobId = providedJobId || generateJobId ( ) ;
219236 // Store the actual API path (without leading slash) as endpointId
220237 const jobEndpointId = targetPath . replace ( / ^ \/ / , '' ) ;
@@ -234,15 +251,18 @@ export async function POST(request: Request) {
234251 createdAt : new Date ( ) . toISOString ( ) ,
235252 costCredits : estimatedPrice ,
236253 } ;
237- if ( providedJobId ) {
238- // Preserve the stub's original createdAt (set when the user clicked Execute).
239- const jobUpdate : Partial < Job > = { ...job } ;
240- delete jobUpdate . createdAt ;
241- const updated = updateJob ( jobId , jobUpdate ) ;
242- // Stub missing (e.g. cleared before the proxy ran) — fall back to creating it.
243- if ( ! updated ) addJob ( job ) ;
244- } else {
245- addJob ( job ) ;
254+ if ( ! isPriceCalc ) {
255+ if ( providedJobId ) {
256+ // Preserve the stub's original createdAt (set when the user clicked Execute).
257+ const jobUpdate : Partial < Job > = { ...job } ;
258+ delete jobUpdate . createdAt ;
259+ const updated = updateJob ( jobId , jobUpdate ) ;
260+ // Stub missing (e.g. cleared before the proxy ran) — fall back to creating it.
261+ if ( ! updated ) addJob ( job ) ;
262+ } else {
263+ addJob ( job ) ;
264+ }
265+ persistedJobId = jobId ;
246266 }
247267
248268 // Make request to deAPI
@@ -256,7 +276,21 @@ export async function POST(request: Request) {
256276
257277 clearTimeout ( timeoutId ) ;
258278
259- const rawResponse = await response . json ( ) ;
279+ // deAPI (or a gateway/proxy in front of it) can return a non-JSON body on
280+ // errors — an HTML 500/502 page, a plain string. response.json() would throw
281+ // a cryptic "Unexpected token '<'". Read the body as text and parse
282+ // defensively so we surface the real HTTP status + a body snippet instead.
283+ const responseText = await response . text ( ) ;
284+ let rawResponse ;
285+ try {
286+ rawResponse = JSON . parse ( responseText ) ;
287+ } catch {
288+ rawResponse = {
289+ error : `Non-JSON response from API (HTTP ${ response . status } )` ,
290+ status : response . status ,
291+ body : responseText . slice ( 0 , 4000 ) ,
292+ } ;
293+ }
260294
261295 // Capture response headers so the UI can optionally display them
262296 const rawResponseHeaders : Record < string , string > = { } ;
@@ -266,13 +300,15 @@ export async function POST(request: Request) {
266300
267301 // Update job with response
268302 if ( ! response . ok ) {
269- updateJob ( jobId , {
270- rawResponse,
271- rawResponseHeaders,
272- status : 'failed' ,
273- error : rawResponse . error || rawResponse . message || `HTTP ${ response . status } ` ,
274- completedAt : new Date ( ) . toISOString ( ) ,
275- } ) ;
303+ if ( ! isPriceCalc ) {
304+ updateJob ( jobId , {
305+ rawResponse,
306+ rawResponseHeaders,
307+ status : 'failed' ,
308+ error : rawResponse . error || rawResponse . message || `HTTP ${ response . status } ` ,
309+ completedAt : new Date ( ) . toISOString ( ) ,
310+ } ) ;
311+ }
276312
277313 return NextResponse . json ( {
278314 success : false ,
@@ -285,12 +321,20 @@ export async function POST(request: Request) {
285321
286322 // For async endpoints, extract request_id
287323 if ( endpoint . isAsync && rawResponse . data ?. request_id ) {
288- updateJob ( jobId , {
289- requestId : rawResponse . data . request_id ,
290- rawResponse,
291- rawResponseHeaders,
292- status : 'processing' ,
293- } ) ;
324+ // A request_id means the job was accepted and QUEUED — not yet being
325+ // computed. Mark it 'pending' (waiting in queue), not 'processing'; the
326+ // WebSocket / reconciliation poll flips it to 'processing' once a worker
327+ // actually starts, then to completed/failed. This avoids the misleading
328+ // "processing" flash (and the processing -> pending regression) right after
329+ // submit.
330+ if ( ! isPriceCalc ) {
331+ updateJob ( jobId , {
332+ requestId : rawResponse . data . request_id ,
333+ rawResponse,
334+ rawResponseHeaders,
335+ status : 'pending' ,
336+ } ) ;
337+ }
294338
295339 return NextResponse . json ( {
296340 success : true ,
@@ -319,7 +363,7 @@ export async function POST(request: Request) {
319363 } else if ( rawResponse . data ?. balance !== undefined ) {
320364 syncUpdateData . costCredits = rawResponse . data . balance ;
321365 }
322- updateJob ( jobId , syncUpdateData ) ;
366+ if ( ! isPriceCalc ) updateJob ( jobId , syncUpdateData ) ;
323367
324368 return NextResponse . json ( {
325369 success : true ,
@@ -332,6 +376,16 @@ export async function POST(request: Request) {
332376 } catch ( error ) {
333377 console . error ( '[deapi-tester] POST /api/proxy error:' , error ) ;
334378 const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
379+ // Don't leave a pre-created job stuck (e.g. in 'sending'/'pending') when the
380+ // request throws before a response was recorded — mark it failed so the UI
381+ // reflects the error instead of spinning forever.
382+ if ( persistedJobId ) {
383+ updateJob ( persistedJobId , {
384+ status : 'failed' ,
385+ error : errorMessage ,
386+ completedAt : new Date ( ) . toISOString ( ) ,
387+ } ) ;
388+ }
335389 return NextResponse . json (
336390 { error : errorMessage } ,
337391 { status : 500 }
0 commit comments