Skip to content

0003 — Prompt content is LF, never CRLF

Status: accepted

Context

Prompt content containing CRLF line endings makes the Azure OpenAI deployment return HTTP 500, deterministically. Not intermittently, not under load — the same content fails every time, and the identical content with LF endings succeeds every time.

Everything about the failure points away from the cause. A 500 from the service reads as their problem. The prompt is valid text either way; nothing rejects it locally, no validation catches it, and the difference is invisible in every editor and in most diffs. It survives a copy-paste between environments intact, so "it works on dv" holds right up until it doesn't.

This is easy to reintroduce without noticing. Pasting a prompt into /Admin/Prompts from a Windows editor, a git checkout with core.autocrlf=true, or a SQL script saved on Windows all produce CRLF. The rows seeded by the old HasData migrations (see 0002) still carry it.

Proven with scripts/ai-probe, which reproduces the failure and delta-debugs the input down to the minimal failing case, producing the apim-request-id evidence an Azure support case needs.

Decision

Prompt content is stored with LF line endings only, whatever route it takes into the database — /Admin/Prompts, a direct SQL UPDATE, or a seeded default.

Startup's drift comparison normalises line endings before comparing, so pre-existing CRLF rows do not show up as false drift. That normalisation is for comparison only — it does not rewrite the stored content, and content actually sent to the model is whatever is in the row.

Consequences

  • Any tooling that writes prompts must write LF. When editing through SQL, use a client that does not helpfully convert, and verify with SELECT CHARINDEX(CHAR(13), Content) FROM Prompts WHERE [Key] = '…' — a non-zero result means a CR is present and that prompt will 500.
  • A 500 from Azure OpenAI immediately after a prompt edit is this until proven otherwise. Check the line endings before reading anything else.
  • The rows still carrying CRLF from the HasData era are only safe because nothing has touched them; if one starts failing, this is why.
  • The real fix is to normalise on write in /Admin/Prompts and in the seeder, so the invariant is enforced rather than remembered. That has not been done — this record exists because it has not.