skip to content

Why does OPA's /v1/data return 200 with an empty JSON body?

level: middleimportance: should knowfreq 52%

answer

  1. the status code stays cheerful
  2. no value means no field
  3. an empty set would still appear
  4. absent result is not a permit

basics

~20 s

Because the document you queried is undefined: nothing produced a value, so there is no result field to return. Evaluation itself succeeded, which is why the status is still 200. An empty body is no decision - neither an allow nor a deny.

solid answer

~50 s

A `200` with the body `{}` means OPA evaluated the query fine but the document you asked for has no value. The usual causes are a complete rule whose body did not hold and that has no default, a mistyped document path, or a package that never made it into the loaded bundle. The response shape helps you tell them apart: a partial set rule that exists but matched nothing is still defined - it comes back as `{"result": []}` - so a bare `{}` on a set-valued path means the document is not there at all. The dangerous part is client-side. A caller that reads the `deny` list and blocks only when it is non-empty sees an empty body as 'no violations' and proceeds, so a bundle that failed to load looks exactly like a clean pass. Treat a missing `result` as a hard failure and alarm on it.

go deeper

for a junior

Remember one fact and you are ahead: an empty body from OPA is neither a yes nor a no. If there is no result field, no decision came back, and the safe move is to stop rather than to continue.

for a middle

Be able to list what leaves a queried document with no value - a rule whose conditions did not hold and that has no fallback, a mistyped path, a bundle that never loaded - and explain why a set-valued path that exists returns an empty list rather than an empty body.

for a senior

Demonstrate that you have hardened a real client: presence check on the result field, fail closed with an alarm, and a test that pins the exact path you query for both an allow and a deny input.

for a principal

Decide what the organisation treats an unanswered policy query as. A silent pass keeps deployments moving and quietly voids the control; a hard failure makes the engine a dependency of every change. Name the choice and what you funded to afford it.

## The 200 that is not an answer OPA's Data API replies to a successful evaluation with `{"result": <value>}`. When the document you queried has no value, there is nothing to put in that field, so OPA omits it - and what arrives on the wire is HTTP `200` with the body `{}`. Evaluation genuinely succeeded; the query simply produced nothing. This is the sharpest edge in the whole decision API, because at the transport layer 'no decision' is indistinguishable from a healthy call. ### What makes a queried document produce nothing Three causes account for nearly all of it, and they are worth being able to separate under pressure: 1. **The rule exists but produced no value on this input.** A complete rule such as `allow` that has no fallback value simply yields nothing when its conditions do not hold. It does not yield `false` - it yields nothing, and nothing has no wire representation inside `result`. 2. **The path does not name anything.** A typo, a stale package name after a refactor, a rule that was renamed. OPA does not treat an unknown document path as an error; it is just another document with no value, so you get the same `200 {}`. 3. **The policy was never loaded.** A bundle that failed to download, failed its signature check, or failed to activate leaves the package absent from the engine. Same response again. ### The response shape can separate them There is one genuinely useful discriminator, and it is the detail that separates a candidate who has integrated OPA from one who has read about it. A **partial set** rule - the `deny` collection style, where each matching case contributes a message - is *defined even when nothing matches*. If the rule exists in a loaded module and no case fires, the document is the empty set and the response is `{"result": []}`, not `{}`. So on a set-valued path such as `/v1/data/db/policy/deny`, a bare `{}` cannot mean 'no violations'. It means the document is not there: wrong path, or the package never loaded. That is an enforcement outage wearing the costume of a clean run, and it should page someone. On a boolean path such as `/v1/data/db/policy/allow`, `{}` is ambiguous between cause 1 and causes 2-3, which is precisely why the fix below matters. ### The failure this causes in real callers Take the retention rule: a managed database may not be created with a backup window under thirty days. The provisioning service queries the policy before it calls the cloud API, and its enforcement code is the natural one - > parse the body, read the list of violations, and if that list is empty, proceed. On a `{}` body there is no list, an absent list has zero entries, and the service proceeds. Every database created during the window in which that bundle was broken went out with a seven-day retention window, and every request in the logs is a `200`. Nothing alerted, because from the outside the integration looked perfectly healthy. This is the fail-open shape of the bug, and it is far more common than the fail-closed one, because 'nothing came back' collapses so naturally into 'nothing to complain about'. ### How to make the caller safe **Assert on presence, not on emptiness.** The client's first check is whether the body has a `result` field at all. If it does not, the query produced no decision: return an error to whoever called you, log the exact path queried, and raise an alarm. An unanswered policy query is an outage of a control, not a permit. **Query a document that is always defined.** Ask the policy team for a single named decision that cannot be undefined - a boolean with a fallback value, or an object that always has a verdict field - rather than pointing your client at a collection it has to interpret as truthy or falsy. Then the presence check above becomes unambiguous: `result` missing means something is broken, full stop, and there is no second reading in which it means approval. **Fail closed, deliberately and visibly.** Decide it once, write it down, and make the alarm loud enough that the fail-closed path is short-lived. The alternative - proceeding when no decision arrived - is a choice too, and it is the choice that voids the control silently. **Prove it in a test.** A contract test against a running engine, asserting that the exact path your client queries returns a `result` for both an allowed and a denied input, catches the package rename and the mistyped path before they reach production. Add one case that feeds your client a `{}` body and asserts it refuses rather than proceeds. And wire your readiness check to OPA's health endpoint with the bundle check enabled, so a bundle that never activated is visible as unhealthy instead of as quiet success. ### The one-line summary `200 {}` means *no value came back*. It is not `false`, it is not `null`, it is not an empty list, and it is certainly not approval - and the client, not the policy, is where that has to be handled.

  • You query a deny path, a partial set, and get {} back. Is that 'no violations'?
    No. A set rule that exists and matches nothing is still defined - it comes back as `{"result": []}`. A bare `{}` on that path means the document is not there: a wrong path, or a bundle that never loaded. Treat it as an enforcement outage, not as a pass.
  • What should the calling service do when the result field is missing?
    Fail closed and alarm. No decision arrived, and 'no decision' is not a permit. Return an error to the caller, log the exact document path you queried, and page - a policy path that stopped answering will otherwise look like a wall of green, successful traffic.
  • How do you catch this before production rather than after?
    Contract-test the integration against a running engine: assert that the exact path your client queries returns a `result` for both an allowed and a denied input, and assert your client rejects a `{}` body. Separately, wire readiness to OPA's health endpoint with the bundle check on, so a bundle that never activated shows up as unhealthy.

A 200 with no result is a phone call that connected and was answered by silence. The line worked; nobody said yes or no.

saying these in an interview costs you the question

  • Reads an empty body as an implicit allow
  • Expects a 404 or a 500 when the document is undefined
  • Says the response is {"result": null} when nothing matched
  • Assumes an empty violations list proves the policy ran

context