{"record":{"id":"130bd7889951f611","repo":"abhigyanpatwari/GitNexus","slug":"job-already-job-status","errorCode":null,"errorMessage":"Job already ${job.status}","messagePattern":"Job already (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"gitnexus/src/server/api.ts","lineNumber":1704,"sourceCode":"      error: job.error,\n      startedAt: job.startedAt,\n      completedAt: job.completedAt,\n    });\n  });\n\n  // GET /api/analyze/:jobId/progress — SSE stream (shared helper)\n  mountSSEProgress(app, '/api/analyze/:jobId/progress', jobManager);\n\n  // DELETE /api/analyze/:jobId — cancel a running analysis job\n  app.delete('/api/analyze/:jobId', requireTrustedOrigin, (req, res) => {\n    const jobId = req.params.jobId as string;\n    const job = jobManager.getJob(jobId);\n    if (!job) {\n      res.status(404).json({ error: 'Job not found' });\n      return;\n    }\n    if (isTerminalJobStatus(job.status)) {\n      res.status(400).json({ error: `Job already ${job.status}` });\n      return;\n    }\n    jobManager.cancelJob(jobId, 'Cancelled by user');\n    res.json({ id: job.id, status: 'failed', error: 'Cancelled by user' });\n  });\n\n  // ── Embedding endpoints ────────────────────────────────────────────\n\n  const embedJobManager = new JobManager();\n\n  // POST /api/embed — trigger server-side embedding generation\n  app.post(\n    '/api/embed',\n    createRouteLimiter({ limit: 20 }),\n    requireTrustedOrigin,\n    async (req, res) => {\n      try {\n        const entry = await resolveRepo(requestedRepo(req));","sourceCodeStart":1686,"sourceCodeEnd":1722,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/0d1aed942f0e8b5d3bac27519fff441aceea722d/gitnexus/src/server/api.ts#L1686-L1722","documentation":"HTTP 400 returned by DELETE /api/analyze/:jobId when the job exists but is already terminal — isTerminalJobStatus covers 'complete' and 'failed' only. Terminal outcomes are immutable by design (updateJob drops later transitions so a racing complete/error cannot flip a result), so there is nothing left to cancel. A successful cancel itself sets status 'failed' with error 'Cancelled by user'.","triggerScenarios":"DELETE after the job completed; DELETE after it failed on its own; a second DELETE after the first succeeded (the job is now 'failed', so you get 400 rather than 404 while the record lives); a cancel racing the final status update.","commonSituations":"UI cancel buttons lagging behind completion; pollers deciding to cancel after observing the terminal state; retry wrappers that cancel-then-restart.","solutions":["Read the status out of the message ('Job already complete' / 'Job already failed') and treat the job as finished — POST /api/analyze again if you want a fresh run","Guard before cancelling: GET /api/analyze/:jobId first and only DELETE when status is non-terminal","Disable cancel controls in the UI once status is complete/failed","Remember cancel = forced 'failed'; don't re-cancel an already-cancelled job"],"exampleFix":"// before\nawait fetch(`/api/analyze/${jobId}`, { method: 'DELETE' }); // 400 once terminal\n\n// after\nconst job = await getJson(`/api/analyze/${jobId}`);\nif (job.status !== 'complete' && job.status !== 'failed') {\n  await fetch(`/api/analyze/${jobId}`, { method: 'DELETE' });\n}","handlingStrategy":"validation","validationCode":"// Only cancel live jobs\nconst { status } = await getJson(`/api/analyze/${jobId}`);\nconst live = status !== 'complete' && status !== 'failed';\nif (live) await fetch(`/api/analyze/${jobId}`, { method: 'DELETE' });","typeGuard":"type JobStatus = 'queued' | 'cloning' | 'analyzing' | 'loading' | 'complete' | 'failed';\nconst isTerminalJobStatus = (s: JobStatus): boolean => s === 'complete' || s === 'failed';","tryCatchPattern":"If the DELETE still returns 400 'Job already …', accept it as the terminal verdict and optionally start a new analysis; do not retry the cancel.","preventionTips":["Poll status immediately before issuing a cancel","Map complete/failed to 'nothing to cancel' in UI state machines","Cancelling sets failed — plan status displays accordingly"],"tags":["http-400","job-scheduler","state-machine","cancel","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"0d1aed942f0e8b5d3bac27519fff441aceea722d","analyzedAt":"2026-08-20T23:29:22.980Z","contentChangedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}