What do the timeout, raise_error and force_cache options on Rego's http.send do?
answer
- One request object in, one response object out
- Evaluation blocks until the call returns
- Failure either raises or returns a field
- Two cache layers, query and cross-query
- Forcing the cache needs an explicit lifetime
basics
~20 sIn Rego's http.send, timeout bounds how long evaluation waits. raise_error decides whether a failed call aborts evaluation or returns an error field. force_cache reuses a response across queries for force_cache_duration_seconds, ignoring the server's cache headers.
solid answer
~40 s`http.send` takes a single request object and returns a response object with `status_code`, `body`, `raw_body` and `headers`. `timeout` bounds the call; evaluation blocks while the request is in flight, so this is the ceiling on the decision's latency, and you set it explicitly rather than relying on the default. `raise_error` defaults to true, meaning a transport failure aborts the whole evaluation with an error instead of returning a decision; set it to false and you get a response object with an `error` field, at which point handling the failure becomes your job in the rule body. `force_cache: true`, together with the required `force_cache_duration_seconds`, caches the response across queries for that many seconds and ignores whatever cache headers the server sent. Within one evaluation, repeated identical calls are already served from an intra-query cache.
code
rego · 19 linespackage licence
deny contains msg if {
some c in input.components
resp := http.send({
"method": "GET",
"url": sprintf("https://licences.internal/class/%s", [c.licence]),
"timeout": "2s",
"raise_error": false,
"force_cache": true,
"force_cache_duration_seconds": 3600,
})
resp.status_code == 200
resp.body.class == "restricted"
msg := sprintf("%s uses restricted licence %s", [c.name, c.licence])
}
# ... nothing here handles resp.error, so an unreachable
# service produces no denials at allgo deeper
Know that a Rego rule can make an HTTP call at all, that it returns an object with a status code and a body, and that the evaluation waits for it.
Explain each option's effect precisely, including that a non-2xx status is a normal response and that a failed call either raises or surfaces as an error field.
Demonstrate the failure analysis: describe exactly what a degraded dependency does to gate latency and verdicts, and what you write so an unreachable service is not a silent pass.
Own the framing that these keys encode policy, not plumbing. Be ready to say who decides the acceptable staleness and who is paged when the lookup becomes the pipeline's bottleneck.
## The call `http.send` takes one object and returns one object: ```rego resp := http.send({ "method": "GET", "url": "https://licences.internal/class/AGPL-3.0", }) # resp.status_code, resp.body, resp.raw_body, resp.headers ``` `body` is the parsed JSON response; `raw_body` is the string. That is the whole surface. Everything interesting is in the options, because the moment a rule makes this call the decision stops being a pure function of `input` and `data` and starts depending on a network hop that happens while a pipeline job or an admission request is waiting. ## timeout Evaluation is synchronous. While the request is outstanding, the query is blocked, and whatever is waiting on the query — a CI step, an admission webhook, an API caller — is blocked too. `timeout` is the bound on that. It accepts a duration string such as `"500ms"` or `"3s"`, or a number interpreted as nanoseconds. A default applies if you omit it, and relying on that default is how a policy that normally answers in single-digit milliseconds becomes a multi-second stall when the licence service is degraded. Set it deliberately, sized against whatever timeout the *caller* enforces, so your gate fails on your terms rather than the caller's. ## raise_error This is the option most people meet the hard way. It defaults to **true**: if the request fails at transport level — DNS failure, connection refused, timeout expiring — the built-in raises, and the entire evaluation ends in an error. The caller does not get a decision; it gets an error. Whether that is a fail-closed block or a fail-open pass is then decided outside Rego, by whatever the calling gate does with an errored evaluation, which is exactly the ambiguity you do not want in a security control. Set `raise_error: false` and the failure comes back in-band: the response object carries an `error` field describing what went wrong, and evaluation continues. That is more controllable, and it is also more dangerous if you forget the next step. A rule written as "deny if the service says restricted" will simply not match when the response is an error object, because `resp.body.class == "restricted"` is undefined. The rule produces nothing, the artifact passes, and no one is told. If you turn off `raise_error`, you must write the error branch explicitly — usually a second rule that denies, or at minimum warns, when `resp.error` is present. Note that a non-2xx HTTP status is *not* a transport error. A 500 or a 404 comes back as an ordinary response with that `status_code` and no error, so a rule that reads `resp.body` without checking the status will happily interpret an error page as a fact. ## Caching There are two layers and they are commonly confused. - **Intra-query**: within a single evaluation, identical `http.send` calls are served from a cache automatically. A rule that iterates two hundred components and calls the same licence URL repeatedly makes one request per distinct URL, not per component. - **Inter-query**: across evaluations, nothing is cached unless you ask. `cache: true` enables caching that honours the response's own HTTP cache headers. `force_cache: true` ignores those headers entirely and caches for `force_cache_duration_seconds`, which must be set alongside it; it takes precedence over `cache`. `force_cache` is what makes a per-request lookup survivable at admission-time volumes. It is also what makes the policy's answer a function of *when* the cache last filled. Two runs over a byte-identical SBOM can disagree because the first was served from a cache entry populated before the licence list changed and the second refetched. Nothing in the policy file records that, which is why anyone reconstructing a past decision needs more than the rule text. ## The judgement behind the options Every option here is really a question about what you want to happen when the fact is unavailable. `timeout` decides how long you are willing to make someone wait for it. `raise_error` decides whether unavailability is an error or a value. `force_cache` decides whether you would rather serve a possibly stale fact than pay for a fresh one. Those are policy decisions dressed as configuration keys, and an interviewer asking about them is usually checking whether you know they are policy decisions.
- The licence service is down and your rule sets raise_error to false. What verdict does the gate return?A clean one. The response object carries an `error` field, so `resp.status_code == 200` is undefined, the body never matches, and the deny set comes back empty. The artifact passes with no signal that the check did not run. You need an explicit rule that denies or warns when `resp.error` is present, otherwise turning off `raise_error` has quietly converted a control into a no-op.
- Does force_cache respect the Cache-Control header the licence service returns?No, that is the point of it. `cache: true` enables inter-query caching that honours the response's own cache headers; `force_cache: true` overrides them and holds the response for `force_cache_duration_seconds`, which must be supplied with it. Use it when the server's headers are absent or too short for the request volume you are putting through the gate, and accept that you have chosen staleness deliberately.
- Your rule calls the same URL once per component in a 300-component SBOM. How many requests go out?One per distinct URL within that evaluation. Identical `http.send` calls are served from an intra-query cache automatically, so 300 components sharing a handful of licences produce a handful of requests. Across separate evaluations that cache does not apply, which is what `cache` or `force_cache` are for.
saying these in an interview costs you the question
- Thinks a 500 response counts as a transport error
- Assumes raise_error false makes the rule fail closed
- Believes force_cache honours the server's cache headers
- Says the call is asynchronous and does not block evaluation
- Sets force_cache without force_cache_duration_seconds