Repository navigation
Description
Is your feature request related to a problem? Please describe.
The HTTP fetch provider always performs a full GET and the client always replaces the data in the policy store, even when the data source has not changed. With periodic_update_interval and update triggers, every client re-downloads and re-writes the full document on every cycle.
We run OPAL in production for an enterprise data platform:
- OPAL server and client 0.9.3 (planning to move to 0.9.9+). The fetcher/updater code paths below are unchanged between 0.9.3 and current
master. - 6–8 opal-client pods (HPA) serving ~1.5k authorization queries/s in total
- One HTTP data source, ~5 MB JSON,
periodic_update_interval600 s
For a document that rarely changes, this is roughly:
- ~144 full fetches per client per day → about 5.8 GB/day of transfer
- ~1,150 full policy-store data replacements per day across the fleet
A data source that supports standard HTTP conditional requests (a strong ETag, If-None-Match → 304 Not Modified) could avoid all of this, but the fetcher never sends the header. Our data source is a PEP-side service we built, and we are adding ETag / 304 support on that server side now — we would like the OPAL side to be able to use it as well.
Code references (current master):
opal_common/fetcher/providers/http_fetch_provider.py—HttpFetcherConfighasheaders / is_json / process_data / method / dataonly; no validator handling. A304would passraise_for_status()and then fail in_response_to_data()on the empty body.opal_client/data/updater.py—calc_hash()is computed for the report only; identical data is still written via_set_policy_data().
Describe the solution you'd like
An opt-in conditional-request mode for the HTTP fetcher (default behaviour unchanged):
- A new
HttpFetcherConfigoption (name open for discussion, e.g.conditional_request: bool = False). - When enabled, after a successful
200the client remembers the responseETag(andLast-Modifiedif present) for that data source entry, and sendsIf-None-Match/If-Modified-Sinceon the next fetch. - On
304 Not Modifiedthe fetcher returns a "not modified" result instead of trying to parse a body, and the updater skips the policy-store write (reported as fetched, not saved). - The validator is kept outside the provider instance (providers are created per fetch), keyed by entry (url + dst_path + config), in client memory only.
Points we would like your input on:
- Policy store resets: if the inline OPA restarts or the store is cleared while the client keeps its validators, a
304would leave the store empty. We think validators should be cleared whenever the policy store is (re)initialised, and probably not used for the base fetch on (re)connect. - Whether you prefer this in the HTTP provider/config as above, or as a generic "not modified" contract in the fetcher engine that other providers could also use.
Describe alternatives you've considered
- Client-side content hash check (skip the store write when
calc_hash()matches the last saved hash). This avoids redundant writes but not the transfer. It could be a complementary change; we can propose it separately if useful. - Longer periodic interval / disabling periodic updates. Reduces cost but also removes the safety net for missed update events.
- A custom fetch provider via
FETCH_PROVIDER_MODULES. Works, but this seems generally useful for anyone fetching large, mostly static documents over HTTP.
Additional context
- We are happy to implement this and open a PR with tests if the direction looks good to you.
- We run the 0.9 line in production. Would a backport to
release/0.9.xbe acceptable, or should this targetmaster(0.10.x) only?
Activity
dshoen619 commentedon Nov 3, 2026
Thanks for the detailed proposal! Notes to plan around:
Design
HttpFetcherConfig, default off, GET only.opal_common.fetcher, returned only when the flag is on. Don't signal it withNone:DataUpdater._fetch_datatreatsNoneas an error.DataUpdaterstores them in memory, keyed by url +dst_path+ a hash of the config, which can hold auth headers.304 handling: catch it before
raise_for_status(). With aiohttp (the default) the 304 gets through, then_response_to_data()raisesContentTypeError. httpx raises on it inside_fetch_, so it gets retried. Please test both clients.Store resets
get_base_policy_data(). It covers initial load, reconnect, inline-OPA rehydration and/data-updater/trigger.Reporting: don't reuse
fetched=True, saved=False(today that means the write failed). Add a defaulted field toDataEntryReport, and mark the transaction successful so/healthyisn't affected.Prior art:
ApiPolicySource(opal_common/sources/api_policy_source.py) already does ETag/304 for policy bundles.Backport: target
masterfirst.release/0.9.xis on pydantic v1, so a port needs the v1 API. Until then, a custom provider that returns a cached body on 304 saves the transfer on 0.9.x.3kd1000 commentedon Nov 4, 2026
Thanks a lot for the detailed notes — this is very helpful. I'll go with the split you described and open a PR against
master:HttpFetcherConfig(default off, GET only). SendsIf-None-Match/If-Modified-Sinceand handles304beforeraise_for_status(), for both aiohttp and httpx (tests for both).opal_common.fetcher: a smallNotModifiedresult type, returned only when the flag is on — noNonesignalling.DataUpdater: keeps the validators in memory, keyed by url +dst_path+ a hash of the config; they travel on the fetch event and come back with the result, so the provider stays stateless. Validators are cleared at the top ofget_base_policy_data().DataEntryReportfor "not modified", with the transaction marked successful so/healthyis unaffected.On external OPA: we're actually planning to move to an external OPA ourselves (to run a newer OPA than the inline one), so I'd like the flag to be safe for both inline and external rather than documenting it as inline-only. Since an external OPA restart isn't visible to the client, a max-age alone would still leave an empty store until it expires. My proposal:
304, check that the policy store still has data at the entry'sdst_path; if it doesn't, drop the validator and do a full fetch.Does that fit what you had in mind, or would you prefer a different signal for "the store was reset" (e.g. a marker written alongside the data)?
I'll use
ApiPolicySourceas the reference for the ETag handling. Thanks again!feat(fetcher): support opt-in HTTP conditional requests (ETag / 304) (p…
feat(fetcher): support opt-in HTTP conditional requests (ETag / 304) (p…