A refused signal says why in a code a client can act on
Three different refusals share one status code, and a client has to tell them apart to say anything useful to an operator. The discriminator is a stable code, not the wording of a message.
The rule
- The signal API refuses in three distinguishable ways, all
409, each carrying a stable machine-readable code beside its human-readable message: the run is in a terminal state and cannot be signalled or resumed (PIPELINE_TERMINAL); an inward signal is already pending for the run (INWARD_SIGNAL_ALREADY_PENDING); and there is no active pause to resume (NO_ACTIVE_PAUSE). - A client classifies a refusal by that code; the message wording is not a contract and an implementation may change it.
- A refusal answers with the error envelope (API-1).
- A newly created cancel or pause signal answers
202; a resume that resolved a pending pause answers200. - A signal addressed to a run that does not exist answers
404to a caller holding blanket authority over runs (existence is no secret from someone who may act on any run), and an opaque403to every other caller, indistinguishable from the answer for a run that exists but is not theirs, so that the route cannot be used to enumerate which runs exist.
What it means
Three different refusals on the signal API share the one status, 409, so
the status alone cannot tell an operator which of the three happened; the
error_code is what does. A code, once published, keeps its meaning
forever — API-8 covers that guarantee for two of the three codes here; the
third, PIPELINE_TERMINAL, is the same contract applied to a run that has
already finished.
Success is not one code either: creating a new signal and resolving one that already existed are different events on the same door, and they answer differently so a caller can tell which happened without inspecting the body.
The existence check on a missing run is scoped to who is asking. A caller
who may already act on any run loses nothing by being told a run does not
exist, so they get an honest 404. Everyone else gets the same opaque
403 whether the run does not exist or simply is not theirs — the two
cases must be indistinguishable, or the difference between them becomes a
way to enumerate which runs exist.
Example
POST /flowdrop/api/pipelines/{pipeline}/cancel
{}POST /flowdrop/api/pipelines/{pipeline}/pause
{}POST /flowdrop/api/pipelines/{pipeline}/resume
{}A caller with blanket authority over runs who cancels one that does not
exist gets 404; every other caller gets 403 for that same request, and
403 again for a run that exists but is someone else's — the two answers
are identical on purpose.
Why
Recorded under OPEN-18.