docs(openapi): declare the statuses routes can already answer - #1167
Merged
Conversation
The contract described the happy path and the timeout but almost nothing in between. 125 declarations are added across eleven controllers; no runtime behaviour changes, and every status added was already reachable before this. The set is derived, not listed. Thirteen error classes in src/common/errors map to a status through their NestJS base, and each route was traced from handler to service to both adapters to see which of them it can reach: 409 EngineNotReadyError 91 routes 501 EngineNotSupported / ChannelMediaNotSupported 11 404 Message/Group/Channel/LabelNotFound 11 400 RecipientUnreachableError 6 403 EngineRefusedError 6 409 is the bulk of it and was the least visible. Every adapter method calls ensureReady() before touching the socket or the page — 168 call sites — and that guard throws EngineNotReadyError, so any route reaching an engine answers 409 when the session has no live engine behind it. Ten routes declared a 409 already, but for an unrelated reason: a name already taken, a plugin already installed, a session already running. Those are semantic conflicts, not a missing engine, and the two sets do not overlap, so the new declaration carries its own description saying which of the two it is. Reachability was type-resolved, not name-matched. An earlier pass keyed on the method name alone and wrongly flagged GET /plugins/catalog, because PluginsService.getCatalog shares a name with CatalogService.getCatalog and only the latter touches an engine. Resolving `this.<prop>.<method>()` through the controller's constructor types removes it; re-running the corrected predicate reports zero remaining mismatches. The predicate was also checked against a known answer: of the 52 routes declaring 503, it flags 49 as engine-reaching and excludes exactly the three whose 503 comes from somewhere else — the readiness probe and the two media-conversion routes. Descriptions live in one shared module rather than per controller. The text describes engine behaviour, not any one route's, so eleven copies would drift the moment one was reworded — which is how the contract fell behind in the first place. They are still applied per handler rather than at class level: a class-level @apiresponse would silently hand a 409 to any future route added to that controller, engine-touching or not. On the four participant writes the undeclared 403 mattered most: it is the status that separates a batch WhatsApp refused outright from one that was applied with some members rejected, which a 200 reports inside `results`.
This was referenced Aug 9, 2026
pull Bot
pushed a commit
to felipefarinha/OpenWA
that referenced
this pull request
Aug 9, 2026
…e's work A self-review of the ten PRs merged this cycle found four things wrong, three of them introduced by rmyndharis#1167 itself. The 409 description was the worst of them. It read "it exists, but no engine is running for it", which describes the case that actually answers 400: the service resolves the engine first and throws BadRequestException('Session is not started') when there is none. ensureReady() fires only when an engine EXISTS and is not READY — disconnected, reconnecting, or still initializing, as its own comment and EngineNotReadyError's doc both say. Confirmed behaviourally against a running instance: three engine routes on a never-started session all answered 400 "Session is not started". The text was published on 91 operations, and the docs site already described the split correctly, so the two disagreed. send-bulk declared a 409 it cannot produce. createBatch's engine check throws a 400, and the batch itself drains through processBatch, which is started fire-and-forget after the handler has already answered 202 — nothing an adapter throws can reach that response. An engine that goes unready mid-batch shows up in the per-message results on GET /messages/batch/{batchId} instead. The nine catalog and status routes gained that 409 while their most likely error stayed undeclared. Both services pass a NotFoundException factory to EngineRegistry.require() rather than taking its BadRequestException default, so an unstarted session is a 404 there. Declaring the rare status and omitting the common one is worse than declaring neither; the 404 is now documented, with wording that flags the divergence from the message and group modules. The divergence itself is left alone — changing it would change behaviour. The logout example showed `pushName: null` and `connectedAt: null` beside a populated `lastActive`. Logout clears `phone` only, so a session that had ever connected keeps both. rmyndharis#1166 made that example schema-valid by adding the two required fields but did not check the ones already there. Contract totals move as expected: 409 101 -> 100, 404 75 -> 84.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The contract described the happy path and the timeout but almost nothing in between. This adds 125 declarations across eleven controllers. No runtime behaviour changes — every status added was already reachable before this PR.
409EngineNotReadyError501EngineNotSupportedError,ChannelMediaNotSupportedError404Message/Group/Channel/LabelNotFoundError400RecipientUnreachableError403EngineRefusedErrorFollows #1163, which did the same for
503alone. The generalisation is that thirteen error classes insrc/common/errorsmap to a status through their NestJS base, not one.The 409, which is most of it
Every adapter method calls
ensureReady()before touching the socket or the page — 168 call sites — and that guard throwsEngineNotReadyError. So any route that reaches an engine answers409when the session exists but has no live engine behind it. None of the 91 said so.Ten routes already declared a
409, for an unrelated reason: a name already taken, a plugin already installed, a session already running. Those are semantic conflicts rather than a missing engine, and the two sets do not overlap, so the new declaration carries its own description distinguishing them.How the route set was derived
Handler → service → both adapters, per route. Two things are worth stating because they changed the answer:
Reachability is type-resolved, not name-matched. An earlier pass keyed on the method name alone and wrongly flagged
GET /plugins/catalog:PluginsService.getCatalogshares a name withCatalogService.getCatalog, and only the latter touches an engine. Resolvingthis.<prop>.<method>()through the controller's constructor types drops it, and re-running the corrected predicate reports zero remaining mismatches.The predicate was checked against a known answer. Of the 52 routes declaring
503, it flags 49 as engine-reaching and excludes exactly the three whose503comes from elsewhere — the readiness probe and the two media-conversion routes. That is the expected split, which is what makes the 91 trustworthy."Session-scoped" was deliberately not used as the criterion, though it was how the scale was first measured:
GET /sessions/{id}/configsits under that prefix and never reaches an engine, so keying on the path shape would have published a status that cannot occur.Two structural choices
Descriptions live in one shared module (
src/common/openapi/engine-status-responses.ts) rather than per controller. The text describes engine behaviour, not any one route's, so eleven copies would drift the moment one was reworded — which is how the contract fell behind in the first place.They are applied per handler, not at class level. Seven of the eleven controllers are 100% engine-touching, so a class-level
@ApiResponsewould have cut the diff from 92 lines to 7. It was rejected: a future route added to one of those controllers would silently inherit a409whether or not it touches an engine, which is the same rot as a hand-maintained list, only harder to notice.The one that mattered most
On the four group participant writes, the undeclared
403is the status separating a batch WhatsApp refused outright from one that was applied with some members rejected — the latter is reported insideresultson a200. Those four now declare200, 403, 409, 503.Verification
openapi:check,lint,format:check,tsc --noEmit,buildall exit 0;npm testpasses 5119 tests across 300 suites.openapi.jsonregenerated withnpm run openapi:export, never hand-edited. Status totals moved exactly as intended:40064→70,40321→27,40464→75,40910→101,50116→27, with503unchanged at 52.