Skip to content

Commit f9b4bad

Browse files
committed
docs: flag cache-window plugin pinning and on-chain submission race
Two subtle behaviors that aren't bugs but bite operators and plugin authors: - Mutation hooks (onBeforeApiCall, onAfterApiCall, onBeforeSign) only fire on the first request in each cache TTL window. Cached hits skip them. Heartbeat / counter / alerting work should go in onResponseSent or onHttpRequest, which fire on every request. - Cached responses are byte-identical (same data, signature, timestamp), so on every on-chain submission past the first the verifier reverts with "Already fulfilled" and the submitter eats gas. For endpoints whose consumers race to fulfill on-chain, set a short maxAge or skip caching entirely. No code changes — just lifting these from the audit notes into the public docs where plugin authors and operators will see them.
1 parent 4b7e754 commit f9b4bad

2 files changed

Lines changed: 25 additions & 0 deletions

File tree

book/docs/config/apis.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ cache:
153153
response until the TTL expires. A cached response replays the same signature and timestamp, so within `maxAge` every
154154
caller (and every on-chain submission) gets the identical signed payload.
155155

156+
:::warning On-chain race within the cache window
157+
Because every caller in a TTL window receives the **byte-identical** signed response, only the first on-chain submission
158+
through `AirnodeVerifier` succeeds. The verifier's `fulfilled[]` mapping treats subsequent submissions of the same
159+
`(endpointId, timestamp, data)` as replays and reverts with `"Already fulfilled"` — the second and third callers pay gas
160+
for a failed transaction.
161+
162+
For endpoints whose consumers race to submit on-chain (price feeds, single-fulfillment auctions), set a short `maxAge`
163+
(e.g. 1000ms) or omit `cache` entirely so each caller gets a fresh signature. Caching is most useful for off-chain
164+
verification flows or endpoints with a single intended on-chain submitter.
165+
:::
166+
156167
## Endpoint-level fields
157168

158169
Each endpoint describes one upstream API route:

book/docs/config/plugins.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ Guidelines:
8484
Plugins run in the order they are declared. For mutation hooks (`onBeforeApiCall`, `onAfterApiCall`, `onBeforeSign`),
8585
each plugin receives the output of the previous one.
8686

87+
## Caching interaction
88+
89+
The HTTP response cache (`apis[].cache.maxAge` or `endpoints[].cache.maxAge`) stores the entire signed response — data,
90+
signature, and timestamp — for the TTL window. Cached responses bypass the upstream API call, which also bypasses every
91+
hook from `onBeforeApiCall` onward:
92+
93+
- `onBeforeApiCall`, `onAfterApiCall`, `onBeforeSign` — **only fire on the first request in each cache window**. Cached
94+
hits within the TTL never invoke them.
95+
- `onHttpRequest`, `onResponseSent`, `onError` — fire on every request, cached or not.
96+
97+
If your plugin tracks per-request signal (heartbeats, counters, alerting), put that work in `onResponseSent` or
98+
`onHttpRequest`. Don't rely on the mutation hooks to run once per HTTP request — they run once per upstream API call,
99+
which can be much rarer.
100+
87101
## Plugin name
88102

89103
There is no `name` field in the config. The plugin's exported `name` (from its default export, or from the object its

0 commit comments

Comments
 (0)