abhigyanpatwari/GitNexus · warning

Job not found

Error message

Job not found

What it means

HTTP 404 (a JSON body, not an event-stream) returned by the SSE progress endpoints — /api/analyze/:jobId/progress and /api/embed/:jobId/progress, both mounted through mountSSEProgress — when the jobId matches no tracked job. The job lifecycle is the same as the poll endpoints: in-memory records, lost on restart, swept ~1h after terminal state. A non-string jobId parameter instead yields 400 from the assertString check before this point.

Source

Thrown at gitnexus/src/server/sse-progress.ts:73

 *
 * `updateJob` already synthesizes exactly one terminal progress event (and
 * refuses every update once the job is terminal, #2264 P3), and it assigns the
 * status BEFORE emitting — so asking the job is both sufficient and the only
 * source that cannot be spoofed by an intermediate phase label.
 */
export const mountSSEProgress = (app: express.Express, routePath: string, jm: JobManager) => {
  app.get(routePath, (req, res) => {
    let jobId: string;
    try {
      jobId = assertString(req.params.jobId, 'jobId');
    } catch (err) {
      const status = err instanceof BadRequestError ? err.status : 400;
      res.status(status).json({ error: err instanceof Error ? err.message : 'Invalid jobId' });
      return;
    }
    const job = jm.getJob(jobId);
    if (!job) {
      res.status(404).json({ error: 'Job not found' });
      return;
    }

    let eventId = 0;
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
      'X-Accel-Buffering': 'no',
    });

    // Send current state immediately
    eventId++;
    res.write(`id: ${eventId}\ndata: ${JSON.stringify(job.progress)}\n\n`);

    // If already terminal, send event and close
    if (isTerminalJobStatus(job.status)) {
      eventId++;

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Probe the JSON poll endpoint (GET /api/analyze/:jobId) when the EventSource errors — 404 there too means the record is gone: re-trigger analysis if needed and reconnect with the fresh id
  2. Open the stream immediately after the 202 and close it when a terminal event arrives
  3. Persist repo identity rather than jobIds; re-POST to mint a new id after restarts
  4. Use the full UUID from the 202 response in the URL

Example fix

// before
const es = new EventSource(`/api/analyze/${jobId}/progress`); // errors on 404, no recovery

// after
const es = new EventSource(`/api/analyze/${jobId}/progress`);
es.onerror = () => {
  fetch(`/api/analyze/${jobId}`).then(async (r) => {
    if (r.status === 404) {
      const { jobId: fresh } = await postAnalyze(body); // restart and reconnect
      reconnect(`/api/analyze/${fresh}/progress`);
    }
  });
};
Defensive patterns

Strategy: fallback

Validate before calling

// Validate the id before opening the stream
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!UUID_RE.test(jobId)) throw new Error(`Invalid jobId: ${jobId}`);

Type guard

const isJobId = (v: unknown): v is string =>
  typeof v === 'string' &&
  /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);

Try / catch

EventSource has no catch — handle onerror: probe the poll endpoint, and on 404 fall back to re-triggering the job and reconnecting with the new id instead of letting the browser retry the dead URL forever.

Prevention

When it happens

Trigger: Opening an EventSource for an expired (post-TTL) or never-existing job; reconnecting an SSE stream after serve restarted; refreshing a progress page whose URL holds an old jobId; a truncated or typo'd UUID.

Common situations: Long-lived progress tabs reconnecting after server upgrades; jobIds kept in URL/session state; EventSource auto-reconnect racing job cleanup.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/6b3062da2cd38a44. Report an issue: GitHub.