{"record":{"id":"06c93df05e232e00","repo":"koala73/worldmonitor","slug":"invalid-simulation-run-id","errorCode":null,"errorMessage":"Invalid simulation run ID","messagePattern":"Invalid simulation run ID","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"server/worldmonitor/forecast/v1/get-simulation-outcome.ts","lineNumber":98,"sourceCode":"    error: '',\n    theaterSummariesJson,\n    processing: false,\n    eligibleTheaterCount: typeof pointer.eligibleTheaterCount === 'number' ? pointer.eligibleTheaterCount : pointer.theaterCount,\n    failedTheaterCount: typeof pointer.failedTheaterCount === 'number' ? pointer.failedTheaterCount : 0,\n    allTheatersFailed: pointer.allTheatersFailed === true,\n    completionStatus: typeof pointer.completionStatus === 'string' ? pointer.completionStatus : '',\n  };\n}\n\nexport const getSimulationOutcome: ForecastServiceHandler['getSimulationOutcome'] = async (\n  ctx: ServerContext,\n  req: GetSimulationOutcomeRequest,\n): Promise<GetSimulationOutcomeResponse> => {\n  if (req.runId !== undefined && req.runId !== '' && (\n    typeof req.runId !== 'string' || req.runId.length > 128\n    || req.runId !== req.runId.trim() || !validateRunId(req.runId)\n  )) {\n    throw new ApiError(400, 'Invalid simulation run ID', '');\n  }\n  // Read path when caller supplied a specific runId:\n  //   1. By-run hit (real outcome) → return it.\n  //   2. By-run hit (tombstone payload) → fall through with the tombstone note text.\n  //   3. By-run miss + runId in queue → return processing=true.\n  //   4. By-run miss + runId not queued → fall through to :latest.\n  // See #3734 U6.\n  if (req.runId) {\n    let byRunRaw: unknown = null;\n    try {\n      byRunRaw = await getRawJson(`${SIMULATION_OUTCOME_BY_RUN_KEY_PREFIX}:${req.runId}`);\n    } catch (err) {\n      console.warn(`[getSimulationOutcome] by-run lookup failed for ${req.runId}: ${err instanceof Error ? err.message : String(err)}`);\n      // Fall through to :latest below.\n    }\n    if (isOutcomePointer(byRunRaw)) {\n      return outcomeToResponse(byRunRaw, '');\n    }","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/server/worldmonitor/forecast/v1/get-simulation-outcome.ts#L80-L116","documentation":"getSimulationOutcome validates an optional req.runId: if supplied non-empty it must be a trimmed string of at most 128 characters passing validateRunId(). Otherwise ApiError(400, 'Invalid simulation run ID') is thrown before any cache/queue lookup.","triggerScenarios":"runId longer than 128 chars; runId with leading/trailing whitespace (' abc123 '); runId failing validateRunId's format (e.g. wrong characters or shape); runId that is not a string at all.","commonSituations":"Truncation/concatenation bugs building the run ID from job metadata; storing run IDs with padded whitespace in a DB column; passing an object or number ID where a string is expected.","solutions":["Echo back the exact runId string received from the run-submission response, unmodified","Validate client-side: typeof id === 'string' && id === id.trim() && id.length <= 128 plus the runId format regex","If you do not have a specific run, omit runId to fall through to the :latest read path"],"exampleFix":"// before\nawait getSimulationOutcome(ctx, { runId: ' ' + job.id });\n// after\nawait getSimulationOutcome(ctx, { runId: job.id.trim() });","handlingStrategy":"validation","validationCode":"if (runId !== undefined && runId !== '') {\n  const ok = typeof runId === 'string' && runId.length <= 128 && runId === runId.trim() && /^[A-Za-z0-9_-]+$/.test(runId);\n  if (!ok) throw new Error('invalid simulation runId (trim, <=128 chars, expected format)');\n}","typeGuard":"const isRunId = (v: unknown): v is string =>\n  typeof v === 'string' && v.length > 0 && v.length <= 128 && v === v.trim() && validateRunId(v);","tryCatchPattern":"try {\n  return await getSimulationOutcome(ctx, { runId });\n} catch (e) {\n  if (e instanceof ApiError && e.status === 400 && e.message === 'Invalid simulation run ID') {\n    return await getSimulationOutcome(ctx, {}); // fall back to :latest\n  }\n  throw e;\n}","preventionTips":["Echo runId strings verbatim from the submission response","Never pad or truncate stored run IDs","Fall back to the latest endpoint when runId is unavailable"],"tags":["validation","identifier","forecast"],"backgroundTag":"invalid-identifier-format","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}