feat(render): document the async render job lifecycle + fix a schema-corrupting YAML bug - #34
Conversation
…corrupting YAML bug
## The gap
`POST /render` is in the spec, but nothing that lets you FINISH an async render is. Both of
these are live in prod right now and reachable through the gateway:
GET https://api.wave.online/v1/render/{jobId} -> 402 (x402 challenge)
GET https://api.wave.online/v1/render/{jobId}/events -> SSE
A 402 rather than a 404/403 is the receipt that matters: the gateway has a scope rule for
these routes and they are PRICED. So an SDK user can start a render and be billed for it,
then find no documented way to poll it or stream its progress. Ported from
`wave-render/docs/openapi.json`, which has carried them all along without ever reaching the
hub spec or the codegen chain.
`POST /v1/render/still` is deliberately NOT ported — it is internal-only (it authenticates
with an internal render token, not a customer key) and does not belong on a public surface.
## RenderJobStatus is intentionally not JobStatus
The platform-wide `JobStatus` is `pending|processing|completed|failed|cancelled`. The render
pipeline actually reports `queued|rendering|delivering|done|error`. Those are not the same
vocabulary, and mapping onto the shared enum would misreport `delivering` — output produced,
upload in flight — as either finished or failed. So `RenderJobStatus` is its own enum that
matches what the service emits.
## Drive-by: a real bug that was corrupting generated SDKs
`RenderResultUrl.properties.url` was written as an unquoted YAML flow mapping containing
commas:
url: { type: string, description: Signed, single-object, expiring URL on downloads... }
YAML splits on those commas, so the schema parsed as:
{"type":"string","description":"Signed",
"single-object":null,"expiring URL on downloads.wave.online.":null}
The description was truncated to the single word "Signed" and TWO junk properties were
injected into the schema — which every generated SDK inherits. Quoted the string. Swept the
whole document for the same shape; this was the only instance.
## Receipts
redocly lint openapi.yaml
before: Validation failed with 2 errors and 53 warnings (both errors = this bug)
after: Woohoo! Your API description is valid. 0 errors, 53 warnings (pre-existing)
internal $ref resolution: 78/78 resolve, 0 dangling
paths 41 -> 43, schemas 67 -> 69
Refs wave-av/wave-cli#31.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_f768db87-2f68-4d05-974c-1e29fb1549c9) |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 54 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Comment |
The root-cause fix for the schema-corruption bug in the previous commit.
That bug — an unquoted YAML flow mapping whose commas split the value into junk keys,
silently corrupting a schema every generated SDK inherits — was not subtle. `redocly lint`
reported it as a hard ERROR. It survived on `main` because **nothing in this repo ever
linted the spec.** The spec IS the product here: every SDK and the CLI are generated from
`openapi.yaml`, so a spec defect is a fleet-wide defect. It gets a gate.
Two checks, because they catch different things:
1. `redocly lint openapi.yaml` — schema-level validity. Fails on errors; the 53 pre-existing
warnings do not fail the build (redocly exits non-zero only on errors). Tighten to
warnings once that backlog is worked down. Pinned to @redocly/cli@2.40.0.
2. `.github/scripts/assert-refs.mjs` — every internal `$ref` resolves. redocly validates the
document against the OpenAPI schema, but the failure mode that actually bites a
hand-edited spec is a `$ref` at a component that was renamed or never added. That still
PARSES, so YAML tooling is happy, and the generator then emits a broken type for every
consumer downstream.
The resolver descends with `Object.getOwnPropertyDescriptor`, not `node[part]`. That is not
decoration: a plain index/`in` lookup walks the prototype chain, so `#/__proto__/toString`
would report as RESOLVING and the check would silently pass on a dangling ref. It also reads
the slot without firing a getter.
Proven in both directions — a gate that cannot fail is not a gate:
node .github/scripts/assert-refs.mjs openapi.yaml
-> assert-refs: 194 $ref(s) in openapi.yaml, all resolve exit 0
(RenderJobView -> RenderJobViewTypo)
-> ::error::1 dangling $ref(s) #/components/schemas/RenderJobViewTypo exit 1
($ref: "#/__proto__/toString")
-> ::error::1 dangling $ref(s) #/__proto__/toString exit 1
npx @redocly/cli@2.40.0 lint openapi.yaml
-> Woohoo! Your API description is valid. 0 errors
js-yaml is installed explicitly in the step rather than leaned on transitively — `npx
@redocly/cli` resolves in its own temp prefix, so nothing it depends on is importable from
this repo. Verified by the failure that caught me first: `Cannot find package js-yaml`.
Added a .gitignore (the repo had none) so that npm install can never land in a commit.
Action pins verified against the GitHub API, not guessed:
actions/checkout@34e1148 = v4.3.1
actions/setup-node@48b55a0 = v6.4.0 (the org-canonical pin, 179 uses)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First increment of the org-wide API reconciliation (wave-av/wave-cli#31), run spec ← reality: probe what is actually live, then codify it.
The gap
POST /renderis in the hub spec. Nothing that lets you finish an async render is. Both of these are live in prod and reachable through the gateway today:The 402 is the receipt that matters. A 404 or a
ROUTE_NOT_MAPPED403 would mean "not wired." A 402 means the gateway has a scope rule for these routes and they are priced. So an SDK user can start a render, be billed for it, and then find no documented way to poll it or stream progress.They were ported from
wave-render/docs/openapi.json, which has carried them all along without ever reaching the hub spec — so they never enteredapi-spec → sdks/codegen → SDKs → CLI.POST /v1/render/stillis deliberately not ported: it authenticates with an internal render token, not a customer key, and does not belong on a public surface. (Probing it also returns 402, which is exactly why reconciling by probe alone is insufficient — you have to read the auth model too.)RenderJobStatusis intentionally notJobStatusJobStatuspending · processing · completed · failed · cancelledqueued · rendering · delivering · done · errorDifferent vocabularies. Forcing render onto the shared enum would misreport
delivering— output produced, upload in flight — as either finished or failed. SoRenderJobViewuses its ownRenderJobStatus.Drive-by: a bug that was corrupting every generated SDK
RenderResultUrl.properties.urlwas an unquoted YAML flow mapping containing commas:YAML splits on those commas. The schema was parsing as:
{"type": "string", "description": "Signed", "single-object": null, "expiring URL on downloads.wave.online.": null}The description silently truncated to the single word
"Signed", and two junk properties were injected into the schema — inherited by every generated SDK. Quoted the string, then swept the whole document for the same shape; this was the only instance.This was also the entire source of
redocly lint's 2 errors onmain.Receipts
Not in this PR — filed separately
The other spoke specs surfaced distinct problems, tracked as their own issues rather than bundled here:
https://api.wave.online/v1/media, which returnsROUTE_NOT_MAPPED; its 6 live/v1/engine/*paths can't be folded in until that's settled.rt.wave.online, a different host from the hub's single server, and are SDP-based rather than JSON.Correction to an earlier claim: I initially reported a fourth orphaned spec in "wave-video". That was wrong —
wave-av/wave-videoandwave-av/wave-renderare the same repository (renamed), and what I found was a second stale local clone on disk parked on the rename commit. Retracted in wave-av/wave-render#240. The sweep counted directories instead of resolvinggit remote get-url origin.🤖 Generated with Claude Code