{"record":{"id":"68015710fe5e017f","repo":"srbhr/Resume-Matcher","slug":"missing-placeholders","errorCode":"missing_placeholders","errorMessage":"Feature prompts validation error (missing_placeholders)","messagePattern":"Feature prompts validation error \\(missing_placeholders\\)","errorType":"error_code","errorClass":"FeaturePromptsError","httpStatus":422,"severity":"warning","filePath":"apps/frontend/lib/api/config.ts","lineNumber":394,"sourceCode":"    method: 'PUT',\n    headers: { 'Content-Type': 'application/json' },\n    credentials: 'include',\n    body: JSON.stringify(update),\n  });\n\n  if (!res.ok) {\n    // Error path: body may be absent or malformed, so we tolerate parse\n    // failure. A fetch body is a one-shot stream — read it once and reuse\n    // for both the 422-special-case and the generic fallback.\n    const errBody = (await res.json().catch(() => ({}))) as {\n      detail?: FeaturePromptsValidationError | string;\n    };\n    if (\n      res.status === 422 &&\n      typeof errBody.detail === 'object' &&\n      errBody.detail?.code === 'missing_placeholders'\n    ) {\n      throw new FeaturePromptsError(errBody.detail);\n    }\n    // FastAPI can return ``detail`` as a string or a structured object.\n    // Stringifying an object via the ``||`` shortcut yields \"[object Object]\";\n    // serialize explicitly.\n    let message: string;\n    if (typeof errBody.detail === 'string') {\n      message = errBody.detail;\n    } else if (errBody.detail) {\n      message = JSON.stringify(errBody.detail);\n    } else {\n      message = `Failed to update feature prompts (status ${res.status}).`;\n    }\n    throw new Error(message);\n  }\n\n  // Success path: require a valid JSON body. Swallowing parse errors here\n  // would let an invalid success response be returned as FeaturePrompts\n  // with undefined fields — caller code would then read .cover_letter_prompt","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/frontend/lib/api/config.ts#L376-L412","documentation":"updateFeaturePrompts throws a FeaturePromptsError when the backend answers HTTP 422 with a structured detail whose code is 'missing_placeholders'. This means the server validated the submitted prompt templates and found required {placeholder} tokens (e.g. {job_description}, {resume}) absent from one of the prompts. Unlike the sibling errors this one carries the parsed detail object, letting the UI highlight exactly which prompt/placeholder is at fault.","triggerScenarios":"Saving custom cover-letter or cold-outreach prompt text where a required placeholder was deleted or renamed — e.g. a user edits the cover_letter_prompt in Settings and removes '{job_description}' or '{company_name}', or a migration/import strips placeholders, or the frontend sends a prompt built without the tokens the backend template contract requires.","commonSituations":"Users hand-editing prompts in the Settings feature-prompts form and pasting text over the template placeholders; locale/translation flows that swap placeholder names; automated tests or scripts posting prompt JSON copied from an older schema.","solutions":["Read err.detail (the FeaturePromptsError payload) to find which prompt is missing which placeholder, then re-insert the exact {placeholder} tokens in the Settings editor.","Validate the prompt client-side before submitting: every required placeholder for the feature must appear in the string.","If placeholders were lost via copy/paste, restore from the default prompt shown in the form and re-apply edits around the tokens.","Catch FeaturePromptsError separately in the save handler and render a targeted message instead of the generic save error."],"exampleFix":"// before\nawait updateFeaturePrompts({ cover_letter_prompt: 'Write a cover letter for this job.' });\n\n// after\nconst prompt = 'Write a cover letter for {job_title} at {company_name} using {resume}.';\nconst required = ['{job_title}', '{company_name}', '{resume}'];\nif (required.some((p) => !prompt.includes(p))) {\n  throw new Error('Prompt is missing required placeholders: ' + required.join(', '));\n}\nawait updateFeaturePrompts({ cover_letter_prompt: prompt });","handlingStrategy":"validation","validationCode":"const REQUIRED: Record<string, string[]> = {\n  cover_letter_prompt: ['{job_title}', '{company_name}', '{resume}'],\n  cold_outreach_prompt: ['{job_title}', '{company_name}', '{resume}'],\n};\nfunction missingPlaceholders(prompt: string, required: string[]): string[] {\n  return required.filter((p) => !prompt.includes(p));\n}","typeGuard":"function isFeaturePromptsError(e: unknown): e is FeaturePromptsError {\n  return e instanceof FeaturePromptsError;\n}","tryCatchPattern":"try {\n  await updateFeaturePrompts(update);\n} catch (e) {\n  if (e instanceof FeaturePromptsError) {\n    highlightMissingPlaceholders(e.detail);\n  } else throw e;\n}","preventionTips":["Validate placeholders in the form before submit, not just server-side","Keep placeholders visually distinct (highlighted tokens) in prompt editors","Restore defaults button to recover templates with intact placeholders","Test prompt round-trips after i18n or template changes"],"tags":["validation","http-422","prompts","frontend-api-client"],"backgroundTag":"prompt-template-missing-placeholders","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}