Verbatim symptom
Drafting is briefly unavailable# The Error Message That Lied
There's a particular kind of bug that hides behind a polite sentence. This one lived in the hilyt repository from 2026-08-01 until 2026-11-03, and for all of that time it told visitors something kind and false: "Drafting is briefly unavailable".
Briefly. Like it would pass.
Symptom
The /try page is where a visitor first meets the product. They give it some input, it calls the draft-profile edge function, and a draft comes back. That's the whole job.
What visitors actually saw was the string "Drafting is briefly unavailable". No draft, nothing else. And that wording matters more than you'd think, because it reads like a transient outage. Wait a bit, try again, the servers are having a moment. It doesn't read like "this will never work for anyone, in any browser, ever", which is what was true.
I keep coming back to that. The message was written as a graceful fallback for a flaky network, and it ended up as a very convincing cover story for a permanent configuration fault.
It was found on 2026-11-03, by the new nightly QA journeys. Real people, presumably, had been bouncing off it for a long while.
What we thought it was
The first guess was the obvious one: the function was misbehaving. Slow cold start, an upstream hiccup, something erroring on the server side. And honestly, that guess is believable, because the message itself pushes you there. "Briefly unavailable" is the app telling you the service is down, so you go and look at the service.
You check whether the function is deployed. It is. You check whether it responds. It does, if you call it directly. So you start wondering about timing, retries, whether the failure is intermittent and the QA run just caught it on a bad night.
That's the trap. Every direct check came back healthy, which pointed away from the function and towards flakiness, when the function was never the problem. A healthy server plus a failing page is a very particular signature, and I didn't read it properly at first. A request that never arrives can't produce a server-side error. You'd just see silence on the server and a failure in the browser.
Root cause
The page calls the function through supabase.functions.invoke. Here's the bit that's easy to forget: that client doesn't send a bare request. It sends authorization, apikey and x-client-info, alongside content-type. Four headers.
Four headers is where it gets awkward, because when a browser makes a cross-origin request with custom headers, it doesn't just fire it. It sends a preflight first, an OPTIONS request asking the server which headers are allowed. Only if the answer covers everything it intends to send will it go ahead with the real request.
The function's preflight response, at supabase/functions/draft-profile/index.ts:21@bdf49fd^, listed only content-type in Access-Control-Allow-Headers. One header out of four.
So the browser did exactly what browsers are built to do. It read the allow-list, saw that authorization, apikey and x-client-info weren't on it, and refused to send the real request. The function never saw it. Nothing to log, nothing to fail.
Then the client-side code did its own thing: it caught the failed call and showed its fallback copy. "Drafting is briefly unavailable". Two layers, each behaving sensibly in isolation, combining into something misleading.
And that's why the direct checks all passed. Call the function from a terminal and there's no browser enforcing CORS, so no preflight, so no problem. The fault only exists where the browser is involved, which is precisely where the visitors were.
The fix
The change is small, which is a bit humbling. The preflight response now lists all four headers: content-type, authorization, apikey and x-client-info. That's it.
It landed across two commits, 14adbdb and bdf49fd, and shipped on 2026-11-03, the same day it was detected. The caret in that code reference, bdf49fd^, points at the parent of bdf49fd, meaning the state of the file just before the change. That's the version where the list held a single header.
Same-day detection and fix is a nice outcome, but I wouldn't take too much comfort from it. The gap between introduced and detected is the real number here, and it's the one worth thinking about.
Why it happened
The line was correct when it was written, probably. If your function only ever received content-type, an allow-list of content-type is complete. The trouble starts when the caller changes, or when the assumption about the caller was wrong from the start, and nothing in the code connects the two.
That's the systemic part. The allow-list lived in the function. The headers lived in a client library the function's author doesn't control. Review looks at a diff, and the diff showed a header list that looked fine on its face. There was nothing wrong visible in the lines themselves; the wrongness was in what they omitted relative to a caller outside the diff.
Add the fallback message on top. A failure that announces itself loudly gets fixed quickly. This one was dressed up as weather. Nobody was paged by a visitor seeing something described as brief.
And there was no test that exercised the real path: a browser, a preflight, the actual invoke call. Server-side checks were green because the server was fine. The thing that finally caught it was a journey that behaves like a visitor, which tells you what the earlier checks were missing.
How to avoid it
First, treat the CORS allow-list as a contract with your client library, not a local detail. If you call functions through a wrapper, find out what headers it sends and write the list from that, not from memory.
Second, test through a browser at least once, on the real route. Calling the endpoint directly proves the server works; it proves nothing about the preflight.
Third, be suspicious of any error copy that promises a short wait. If a message implies a temporary condition, make sure something is actually distinguishing temporary from permanent, or the wording will happily mask the second as the first.
Anyway, that's the lot. One header out of four, and a sentence that said "briefly" for a very long time.
Every commit, file reference and date above is checked
against this incident's Noctem evidence pack before publication. Evidence:
cors-preflight-allowed-only-content-type-supabase-invoke.