From c589368c9a73bb1cb529f7fb0b0fe03303e947f1 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Mon, 22 Jun 2026 14:06:35 +0800 Subject: [PATCH] fix(oas-validator): negatively cache spec_url fetch failures When spec_url is configured, a fetch or compile failure was not cached, so a persistently failing spec_url triggered an outbound HTTP request on every incoming request (failure amplification). Add neg_ttl/neg_count to the spec lru cache so failures are cached for a short window (5s), while a recovered spec_url is still picked up quickly. Add a test asserting a failing spec_url is fetched once across several requests. Signed-off-by: AlinsRan --- apisix/plugins/oas-validator.lua | 5 +++ t/plugin/oas-validator3.t | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua index 0cdd5b4a018c..81136dc5e277 100644 --- a/apisix/plugins/oas-validator.lua +++ b/apisix/plugins/oas-validator.lua @@ -31,6 +31,9 @@ local tab_concat = table.concat local plugin_name = "oas-validator" local DEFAULT_SPEC_URL_TTL = 3600 +-- Cache fetch/compile failures for a short window so a persistently failing +-- spec_url is not re-fetched on every request (failure amplification). +local SPEC_URL_NEG_TTL = 5 local schema = { type = "object", @@ -131,6 +134,8 @@ local function get_spec_url_lrucache() spec_url_lrucache = core.lrucache.new({ ttl = ttl, count = 512, + neg_ttl = SPEC_URL_NEG_TTL, + neg_count = 512, invalid_stale = true, refresh_stale = true, serial_creating = true, diff --git a/t/plugin/oas-validator3.t b/t/plugin/oas-validator3.t index cdd2b90104e9..6f353c827ad7 100644 --- a/t/plugin/oas-validator3.t +++ b/t/plugin/oas-validator3.t @@ -25,8 +25,16 @@ add_block_preprocessor(sub { my ($block) = @_; my $http_config = $block->http_config // <<_EOC_; + lua_shared_dict oas_neg_count 1m; server { listen 1979; + location /count-fail.json { + content_by_lua_block { + ngx.shared.oas_neg_count:incr("n", 1, 0) + ngx.status = 404 + ngx.print("not found") + } + } location /spec.json { content_by_lua_block { local file = io.open("t/spec/spec.json", "r") @@ -562,3 +570,70 @@ status: 200 } --- response_body passed + + + +=== TEST 24: create route whose spec_url always fails (for negative cache) +--- config + location /t { + content_by_lua_block { + ngx.shared.oas_neg_count:set("n", 0) + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/2', + ngx.HTTP_PUT, + [[{ + "uri": "/negcache", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/count-fail.json" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 25: failing spec_url is fetched once then negatively cached +--- config + location /t { + content_by_lua_block { + -- give the route created in the previous test time to propagate + ngx.sleep(0.3) + local http = require("resty.http") + local port = ngx.var.server_port + for i = 1, 3 do + local httpc = http.new() + local res, err = httpc:request_uri( + "http://127.0.0.1:" .. port .. "/negcache", + { method = "POST", body = "{}", + headers = { ["Content-Type"] = "application/json" } }) + if not res then + ngx.say("request failed: " .. err) + return + end + if res.status ~= 500 then + ngx.say("unexpected status: " .. res.status) + return + end + end + ngx.say("fetches=" .. (ngx.shared.oas_neg_count:get("n") or 0)) + } + } +--- response_body +fetches=1 +--- error_log +spec URL returned status 404