From 163efd731b10bb4527b4064847ae7f96f0c32e8b Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 09:02:04 +0800 Subject: [PATCH 01/13] feat: add oas-validator plugin The oas-validator plugin validates inbound HTTP requests against an OpenAPI Specification (OAS) 3.x document before forwarding them to upstream services. Requests that fail validation are rejected with a configurable HTTP error status code. The OpenAPI spec can be provided as an inline JSON string or fetched from a remote URL. Remote specs are cached for a configurable TTL (configured via plugin metadata). Validation covers request method, path, query parameters, headers, and request body, with per-category skip flags for selective enforcement. Dependencies: - lua-resty-openapi-validator (>= 1.0.5-1) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apisix-master-0.rockspec | 1 + apisix/cli/config.lua | 1 + apisix/plugins/oas-validator.lua | 281 +++++++ docs/en/latest/config.json | 1 + docs/en/latest/plugins/oas-validator.md | 480 ++++++++++++ docs/zh/latest/config.json | 1 + docs/zh/latest/plugins/oas-validator.md | 480 ++++++++++++ t/plugin/oas-validator.t | 815 ++++++++++++++++++++ t/plugin/oas-validator2.t | 946 ++++++++++++++++++++++++ 9 files changed, 3006 insertions(+) create mode 100644 apisix/plugins/oas-validator.lua create mode 100644 docs/en/latest/plugins/oas-validator.md create mode 100644 docs/zh/latest/plugins/oas-validator.md create mode 100644 t/plugin/oas-validator.t create mode 100644 t/plugin/oas-validator2.t diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec index e92266db4ea8..c9d9c73be910 100644 --- a/apisix-master-0.rockspec +++ b/apisix-master-0.rockspec @@ -46,6 +46,7 @@ dependencies = { "lua-resty-hmac-ffi = 0.06-1", "lua-resty-cookie = 0.4.1-1", "lua-resty-session = 4.1.5-1", + "lua-resty-openapi-validator = 1.0.5-1", "opentracing-openresty = 0.1-0", "lua-resty-radixtree = 2.9.2-0", "lua-protobuf = 0.5.3-1", diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 956eef30c267..52be5b0ed50b 100644 --- a/apisix/cli/config.lua +++ b/apisix/cli/config.lua @@ -244,6 +244,7 @@ local _M = { "limit-conn", "limit-count", "limit-req", + "oas-validator", "gzip", -- deprecated and will be removed in a future release -- "server-info", diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua new file mode 100644 index 000000000000..6056aa0edc6b --- /dev/null +++ b/apisix/plugins/oas-validator.lua @@ -0,0 +1,281 @@ +local core = require("apisix.core") +local secret = require("apisix.secret") +local plugin = require("apisix.plugin") +local ov = require("resty.openapi_validator") +local http = require("resty.http") +local ngx_req = ngx.req +local ngx_md5 = ngx.md5 +local pairs = pairs +local ipairs = ipairs +local tostring = tostring +local tab_sort = table.sort +local tab_concat = table.concat + +local plugin_name = "oas-validator" + +local DEFAULT_SPEC_URL_TTL = 3600 + +local schema = { + type = "object", + properties = { + spec = { + description = "schema against which the request/response will be validated", + type = "string", + minLength = 1 + }, + spec_url = { + description = "URL to fetch the OpenAPI spec from", + type = "string", + pattern = [[^https?://]], + }, + spec_url_request_headers = { + description = "custom HTTP headers to include when fetching spec_url", + type = "object", + additionalProperties = { + type = "string", + }, + }, + ssl_verify = { + description = "whether to verify SSL certificate when fetching spec_url", + type = "boolean", + default = false, + }, + timeout = { + description = "HTTP request timeout in milliseconds for fetching spec_url", + type = "integer", + minimum = 1000, + maximum = 60000, + default = 10000, + }, + verbose_errors = { + type = "boolean", + default = false + }, + skip_request_body_validation = { + type = "boolean", + default = false + }, + skip_request_header_validation = { + type = "boolean", + default = false + }, + skip_query_param_validation = { + type = "boolean", + default = false + }, + skip_path_params_validation = { + type = "boolean", + default = false + }, + reject_if_not_match = { + type = "boolean", + default = true + }, + rejection_status_code = { + description = "HTTP status code to return when request validation fails", + type = "integer", + minimum = 400, + maximum = 599, + default = 400 + } + }, + oneOf = { + {required = {"spec"}}, + {required = {"spec_url"}}, + }, +} + +local metadata_schema = { + type = "object", + properties = { + spec_url_ttl = { + description = "TTL in seconds for cached spec fetched from spec_url", + type = "integer", + minimum = 1, + default = DEFAULT_SPEC_URL_TTL, + }, + }, +} + +local spec_url_lrucache +local spec_url_lrucache_ttl + +local function get_spec_url_ttl() + local metadata = plugin.plugin_metadata(plugin_name) + if metadata and metadata.value and metadata.value.spec_url_ttl then + return metadata.value.spec_url_ttl + end + return DEFAULT_SPEC_URL_TTL +end + +local function get_spec_url_lrucache() + local ttl = get_spec_url_ttl() + if not spec_url_lrucache or spec_url_lrucache_ttl ~= ttl then + spec_url_lrucache = core.lrucache.new({ + ttl = ttl, + count = 512, + invalid_stale = true, + refresh_stale = true, + serial_creating = true, + }) + spec_url_lrucache_ttl = ttl + end + return spec_url_lrucache +end + +local function fetch_and_compile(conf) + local httpc = http.new() + httpc:set_timeout(conf.timeout or 10000) + + local params = { + method = "GET", + ssl_verify = conf.ssl_verify or false, + } + if conf.spec_url_request_headers then + params.headers = conf.spec_url_request_headers + end + + local res, err = httpc:request_uri(conf.spec_url, params) + if not res then + return nil, "failed to fetch spec from URL: " .. err + end + + if res.status ~= 200 then + return nil, "spec URL returned status " .. res.status + end + + local validator, err = ov.compile(res.body) + if not validator then + return nil, "failed to compile openapi spec fetched from URL: " .. err + end + + return validator +end + +local _M = { + version = 0.1, + priority = 510, + name = plugin_name, + schema = schema, + metadata_schema = metadata_schema, +} + + +function _M.check_schema(conf, schema_type) + if schema_type == core.schema.TYPE_METADATA then + return core.schema.check(metadata_schema, conf) + end + + local ok, err = core.schema.check(schema, conf) + if not ok then + return false, err + end + + if conf.spec and not secret.is_secret_ref(conf.spec) then + local _, decode_err = core.json.decode(conf.spec) + if decode_err then + return false, "invalid JSON string provided, err: " .. decode_err + end + end + + return true +end + + +local function get_validator(conf) + if conf.spec then + if not conf._validator then + local validator, err = ov.compile(conf.spec) + if not validator then + return nil, "failed to compile openapi spec, err: " .. err + end + conf._validator = validator + end + return conf._validator + end + + local lrucache = get_spec_url_lrucache() + local ssl_verify = conf.ssl_verify or false + local cache_key = conf.spec_url .. "#ssl_verify=" .. tostring(ssl_verify) + if conf.spec_url_request_headers then + local sorted_keys = {} + for k in pairs(conf.spec_url_request_headers) do + sorted_keys[#sorted_keys + 1] = k + end + tab_sort(sorted_keys) + local parts = {} + for _, k in ipairs(sorted_keys) do + parts[#parts + 1] = k .. "=" .. conf.spec_url_request_headers[k] + end + cache_key = cache_key .. "#" .. ngx_md5(tab_concat(parts, "&")) + end + local validator, err = lrucache(cache_key, nil, fetch_and_compile, conf) + if not validator then + return nil, err + end + return validator +end + + +function _M.access(conf, ctx) + local validator, err = get_validator(conf) + if not validator then + core.log.error(err) + return 500, {message = "failed to parse openapi spec"} + end + + local req_body + if not conf.skip_request_body_validation then + local body, body_err = core.request.get_body() + if body_err ~= nil then + core.log.error("failed reading request body, err: " .. body_err) + return 500, {message = "error reading the request body. err: " .. body_err} + end + req_body = body + end + + local headers + if not conf.skip_request_header_validation then + local h, h_err = ngx_req.get_headers(0, true) + if h_err ~= nil then + core.log.error("failed reading request headers, err: " .. h_err) + return 500, {message = "error reading the request headers, err: " .. h_err} + end + headers = h + end + + local query + if not conf.skip_query_param_validation then + query = core.request.get_uri_args(ctx) + end + + local ok, validate_err = validator:validate_request({ + method = core.request.get_method(), + path = ctx.var.uri, + query = query, + headers = headers, + body = req_body, + content_type = ctx.var.content_type, + }, { + path = conf.skip_path_params_validation, + query = conf.skip_query_param_validation, + header = conf.skip_request_header_validation, + body = conf.skip_request_body_validation, + }) + + if not ok then + core.log.error("error occurred while validating request [" .. + core.request.get_method() .. " " .. ctx.var.uri, + "], err: " .. validate_err) + + if conf.reject_if_not_match then + if not conf.verbose_errors then + validate_err = "" + end + return conf.rejection_status_code, + {message = "failed to validate request. " .. validate_err} + end + end +end + +return _M diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index d24eacc3f8e9..e5459e5bdfc5 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -162,6 +162,7 @@ "plugins/limit-count", "plugins/proxy-cache", "plugins/request-validation", + "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", diff --git a/docs/en/latest/plugins/oas-validator.md b/docs/en/latest/plugins/oas-validator.md new file mode 100644 index 000000000000..c37ebd1c2c6a --- /dev/null +++ b/docs/en/latest/plugins/oas-validator.md @@ -0,0 +1,480 @@ +--- +title: oas-validator +keywords: + - Apache APISIX + - API Gateway + - Plugin + - oas-validator + - OpenAPI + - request validation +description: The oas-validator Plugin validates incoming HTTP requests against an OpenAPI Specification (OAS) 3.x document, rejecting non-conforming requests before they reach the upstream service. +--- + + + + + + + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Description + +The `oas-validator` Plugin validates incoming HTTP requests against an [OpenAPI Specification (OAS) 3.x](https://swagger.io/specification/) document before forwarding them to the upstream service. It can validate the request method, path, query parameters, request headers, and body. + +The OpenAPI spec can be provided as an inline JSON string or fetched from a remote URL with configurable caching. Validation failures return a configurable HTTP error status, and detailed error messages can optionally be included in the response body. + +## Attributes + +| Name | Type | Required | Default | Valid values | Description | +|------|------|----------|---------|--------------|-------------| +| spec | string | No* | | | Inline OpenAPI 3.x specification in JSON format. Required if `spec_url` is not set. | +| spec_url | string | No* | | `^https?://` | URL to fetch the OpenAPI specification from. Required if `spec` is not set. | +| spec_url_request_headers | object | No | | | Custom HTTP request headers sent when fetching `spec_url`. Useful for authenticated specification endpoints. | +| ssl_verify | boolean | No | false | | Whether to verify the TLS certificate when fetching `spec_url`. | +| timeout | integer | No | 10000 | [1000, 60000] | HTTP request timeout in milliseconds for fetching `spec_url`. | +| verbose_errors | boolean | No | false | | When `true`, include detailed validation error messages in the response body. | +| skip_request_body_validation | boolean | No | false | | Skip validation of the request body. | +| skip_request_header_validation | boolean | No | false | | Skip validation of request headers. | +| skip_query_param_validation | boolean | No | false | | Skip validation of query string parameters. | +| skip_path_params_validation | boolean | No | false | | Skip validation of path parameters. | +| reject_if_not_match | boolean | No | true | | When `true`, reject requests that fail validation. When `false`, log the validation failure and allow the request through. | +| rejection_status_code | integer | No | 400 | [400, 599] | HTTP status code to return when a request fails validation. | + +\* Exactly one of `spec` or `spec_url` must be provided. + +### Plugin Metadata + +The following metadata attributes control behavior at the plugin level and are configured through the Plugin Metadata API: + +| Name | Type | Required | Default | Valid values | Description | +|------|------|----------|---------|--------------|-------------| +| spec_url_ttl | integer | No | 3600 | ≥ 1 | Time in seconds to cache a specification fetched from `spec_url`. | + +## Examples + +The examples below demonstrate how you can configure `oas-validator` in different scenarios. + +:::note + +You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: + +```bash +admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') +``` + +::: + +### Validate Requests with an Inline Specification + +The following example demonstrates how to validate requests against an inline OpenAPI 3.x specification. Requests that do not conform to the spec are rejected with a `400` response. + + + + + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "oas-validator-route", + "uri": "/api/v3/*", + "plugins": { + "oas-validator": { + "spec": "{\"openapi\":\"3.0.2\",\"info\":{\"title\":\"Pet API\",\"version\":\"1.0.0\"},\"paths\":{\"/api/v3/pet\":{\"post\":{\"requestBody\":{\"required\":true,\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"name\"],\"properties\":{\"name\":{\"type\":\"string\"},\"status\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"description\":\"OK\"}}}}}}", + "verbose_errors": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": {"httpbin.org:80": 1} + } + }' +``` + + + + + +```yaml title="adc.yaml" +services: + - name: httpbin + routes: + - name: oas-validator-route + uris: + - /api/v3/* + plugins: + oas-validator: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true + upstream: + type: roundrobin + nodes: + - host: httpbin.org + port: 80 + weight: 1 +``` + +Synchronize the configuration to the gateway: + +```shell +adc sync -f adc.yaml +``` + + + + + + + + + +```yaml title="oas-validator-ic.yaml" +apiVersion: v1 +kind: Service +metadata: + namespace: aic + name: httpbin-external-domain +spec: + type: ExternalName + externalName: httpbin.org +--- +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + namespace: aic + name: oas-validator-plugin-config +spec: + plugins: + - name: oas-validator + config: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + namespace: aic + name: oas-validator-route +spec: + parentRefs: + - name: apisix + rules: + - matches: + - path: + type: PathPrefix + value: /api/v3/ + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: oas-validator-plugin-config + backendRefs: + - name: httpbin-external-domain + port: 80 +``` + + + + + +```yaml title="oas-validator-ic.yaml" +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + namespace: aic + name: httpbin-external-domain +spec: + ingressClassName: apisix + externalNodes: + - type: Domain + name: httpbin.org +--- +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + namespace: aic + name: oas-validator-route +spec: + ingressClassName: apisix + http: + - name: oas-validator-route + match: + paths: + - /api/v3/* + upstreams: + - name: httpbin-external-domain + plugins: + - name: oas-validator + enable: true + config: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true +``` + + + + + +Apply the configuration to your cluster: + +```shell +kubectl apply -f oas-validator-ic.yaml +``` + + + + + +Send a valid request with the required `name` field: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"name": "doggie", "status": "available"}' +``` + +You should receive a `200` response from the upstream. + +Send an invalid request without the required `name` field: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"status": "available"}' +``` + +You should receive a `400` response with a validation error message. + +### Validate Requests with a Remote Specification URL + +The following example demonstrates how to fetch the OpenAPI specification from a remote URL. The spec is fetched once and cached for the duration specified by `spec_url_ttl` in the plugin metadata. + + + + + +Configure the plugin metadata to set the cache TTL for the remote spec: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/oas-validator" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "spec_url_ttl": 600 + }' +``` + +Create a Route with the `oas-validator` Plugin: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "oas-validator-url-route", + "uri": "/api/v3/*", + "plugins": { + "oas-validator": { + "spec_url": "https://petstore3.swagger.io/api/v3/openapi.json", + "ssl_verify": false, + "verbose_errors": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": {"httpbin.org:80": 1} + } + }' +``` + + + + + +```yaml title="adc.yaml" +services: + - name: httpbin + routes: + - name: oas-validator-url-route + uris: + - /api/v3/* + plugins: + oas-validator: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true + upstream: + type: roundrobin + nodes: + - host: httpbin.org + port: 80 + weight: 1 +``` + +Synchronize the configuration to the gateway: + +```shell +adc sync -f adc.yaml +``` + + + + + + + + + +```yaml title="oas-validator-url-ic.yaml" +apiVersion: v1 +kind: Service +metadata: + namespace: aic + name: httpbin-external-domain +spec: + type: ExternalName + externalName: httpbin.org +--- +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + namespace: aic + name: oas-validator-url-plugin-config +spec: + plugins: + - name: oas-validator + config: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + namespace: aic + name: oas-validator-url-route +spec: + parentRefs: + - name: apisix + rules: + - matches: + - path: + type: PathPrefix + value: /api/v3/ + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: oas-validator-url-plugin-config + backendRefs: + - name: httpbin-external-domain + port: 80 +``` + + + + + +```yaml title="oas-validator-url-ic.yaml" +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + namespace: aic + name: httpbin-external-domain +spec: + ingressClassName: apisix + externalNodes: + - type: Domain + name: httpbin.org +--- +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + namespace: aic + name: oas-validator-url-route +spec: + ingressClassName: apisix + http: + - name: oas-validator-url-route + match: + paths: + - /api/v3/* + upstreams: + - name: httpbin-external-domain + plugins: + - name: oas-validator + enable: true + config: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true +``` + + + + + +Apply the configuration to your cluster: + +```shell +kubectl apply -f oas-validator-url-ic.yaml +``` + + + + + +Send a request that does not conform to the Petstore spec: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"invalid": "body"}' +``` + +You should receive a `400` response with a detailed validation error message because `verbose_errors` is set to `true`. diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index b765065d39bd..94733997389b 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -151,6 +151,7 @@ "plugins/limit-count", "plugins/proxy-cache", "plugins/request-validation", + "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", diff --git a/docs/zh/latest/plugins/oas-validator.md b/docs/zh/latest/plugins/oas-validator.md new file mode 100644 index 000000000000..37e69e2ae78f --- /dev/null +++ b/docs/zh/latest/plugins/oas-validator.md @@ -0,0 +1,480 @@ +--- +title: oas-validator +keywords: + - Apache APISIX + - API 网关 + - Plugin + - oas-validator + - OpenAPI + - 请求校验 +description: oas-validator 插件根据 OpenAPI Specification(OAS)3.x 文档校验入站 HTTP 请求,在请求到达上游服务前拒绝不合规的请求。 +--- + + + + + + + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## 描述 + +`oas-validator` 插件在请求转发至上游服务之前,根据 [OpenAPI Specification(OAS)3.x](https://swagger.io/specification/) 文档对入站 HTTP 请求进行校验。可校验内容包括请求方法、路径、查询参数、请求头以及请求体。 + +OpenAPI 规范可以以内联 JSON 字符串的形式提供,也可以从远程 URL 获取并配置缓存。校验失败时返回可配置的 HTTP 错误状态码,并可选择在响应体中包含详细的错误信息。 + +## 属性 + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +|------|------|--------|--------|--------|------| +| spec | string | 否* | | | 内联 OpenAPI 3.x 规范(JSON 格式)。未设置 `spec_url` 时必填。 | +| spec_url | string | 否* | | `^https?://` | 获取 OpenAPI 规范的 URL。未设置 `spec` 时必填。 | +| spec_url_request_headers | object | 否 | | | 获取 `spec_url` 时附带的自定义 HTTP 请求头,适用于需要鉴权的规范接口。 | +| ssl_verify | boolean | 否 | false | | 获取 `spec_url` 时是否校验 TLS 证书。 | +| timeout | integer | 否 | 10000 | [1000, 60000] | 获取 `spec_url` 的 HTTP 请求超时时间(毫秒)。 | +| verbose_errors | boolean | 否 | false | | 为 `true` 时,在响应体中返回详细的校验错误信息。 | +| skip_request_body_validation | boolean | 否 | false | | 跳过请求体校验。 | +| skip_request_header_validation | boolean | 否 | false | | 跳过请求头校验。 | +| skip_query_param_validation | boolean | 否 | false | | 跳过查询参数校验。 | +| skip_path_params_validation | boolean | 否 | false | | 跳过路径参数校验。 | +| reject_if_not_match | boolean | 否 | true | | 为 `true` 时,拒绝校验失败的请求;为 `false` 时,仅记录校验失败日志并放行请求。 | +| rejection_status_code | integer | 否 | 400 | [400, 599] | 请求校验失败时返回的 HTTP 状态码。 | + +\* `spec` 与 `spec_url` 必须且只能设置其中一个。 + +### 插件元数据 + +以下元数据属性通过插件元数据 API 进行配置,作用于插件级别: + +| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 | +|------|------|--------|--------|--------|------| +| spec_url_ttl | integer | 否 | 3600 | ≥ 1 | 从 `spec_url` 获取的规范的缓存时间(秒)。 | + +## 示例 + +以下示例演示了如何在不同场景中使用 `oas-validator` 插件。 + +:::note + +你可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量: + +```bash +admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') +``` + +::: + +### 使用内联规范校验请求 + +以下示例演示如何使用内联 OpenAPI 3.x 规范校验请求。不符合规范的请求将以 `400` 响应被拒绝。 + + + + + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "oas-validator-route", + "uri": "/api/v3/*", + "plugins": { + "oas-validator": { + "spec": "{\"openapi\":\"3.0.2\",\"info\":{\"title\":\"Pet API\",\"version\":\"1.0.0\"},\"paths\":{\"/api/v3/pet\":{\"post\":{\"requestBody\":{\"required\":true,\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"required\":[\"name\"],\"properties\":{\"name\":{\"type\":\"string\"},\"status\":{\"type\":\"string\"}}}}}},\"responses\":{\"200\":{\"description\":\"OK\"}}}}}}", + "verbose_errors": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": {"httpbin.org:80": 1} + } + }' +``` + + + + + +```yaml title="adc.yaml" +services: + - name: httpbin + routes: + - name: oas-validator-route + uris: + - /api/v3/* + plugins: + oas-validator: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true + upstream: + type: roundrobin + nodes: + - host: httpbin.org + port: 80 + weight: 1 +``` + +将配置同步到网关: + +```shell +adc sync -f adc.yaml +``` + + + + + + + + + +```yaml title="oas-validator-ic.yaml" +apiVersion: v1 +kind: Service +metadata: + namespace: aic + name: httpbin-external-domain +spec: + type: ExternalName + externalName: httpbin.org +--- +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + namespace: aic + name: oas-validator-plugin-config +spec: + plugins: + - name: oas-validator + config: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + namespace: aic + name: oas-validator-route +spec: + parentRefs: + - name: apisix + rules: + - matches: + - path: + type: PathPrefix + value: /api/v3/ + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: oas-validator-plugin-config + backendRefs: + - name: httpbin-external-domain + port: 80 +``` + + + + + +```yaml title="oas-validator-ic.yaml" +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + namespace: aic + name: httpbin-external-domain +spec: + ingressClassName: apisix + externalNodes: + - type: Domain + name: httpbin.org +--- +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + namespace: aic + name: oas-validator-route +spec: + ingressClassName: apisix + http: + - name: oas-validator-route + match: + paths: + - /api/v3/* + upstreams: + - name: httpbin-external-domain + plugins: + - name: oas-validator + enable: true + config: + spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' + verbose_errors: true +``` + + + + + +将配置应用到集群: + +```shell +kubectl apply -f oas-validator-ic.yaml +``` + + + + + +发送一个包含必填 `name` 字段的合法请求: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"name": "doggie", "status": "available"}' +``` + +将收到来自上游的 `200` 响应。 + +发送一个缺少必填 `name` 字段的非法请求: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"status": "available"}' +``` + +将收到包含校验错误信息的 `400` 响应。 + +### 使用远程规范 URL 校验请求 + +以下示例演示如何从远程 URL 获取 OpenAPI 规范。规范在首次获取后会被缓存,缓存时长由插件元数据的 `spec_url_ttl` 参数决定。 + + + + + +配置插件元数据以设置远程规范的缓存时间: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/oas-validator" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "spec_url_ttl": 600 + }' +``` + +创建带有 `oas-validator` 插件的路由: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ + -H "X-API-KEY: ${admin_key}" \ + -d '{ + "id": "oas-validator-url-route", + "uri": "/api/v3/*", + "plugins": { + "oas-validator": { + "spec_url": "https://petstore3.swagger.io/api/v3/openapi.json", + "ssl_verify": false, + "verbose_errors": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": {"httpbin.org:80": 1} + } + }' +``` + + + + + +```yaml title="adc.yaml" +services: + - name: httpbin + routes: + - name: oas-validator-url-route + uris: + - /api/v3/* + plugins: + oas-validator: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true + upstream: + type: roundrobin + nodes: + - host: httpbin.org + port: 80 + weight: 1 +``` + +将配置同步到网关: + +```shell +adc sync -f adc.yaml +``` + + + + + + + + + +```yaml title="oas-validator-url-ic.yaml" +apiVersion: v1 +kind: Service +metadata: + namespace: aic + name: httpbin-external-domain +spec: + type: ExternalName + externalName: httpbin.org +--- +apiVersion: apisix.apache.org/v1alpha1 +kind: PluginConfig +metadata: + namespace: aic + name: oas-validator-url-plugin-config +spec: + plugins: + - name: oas-validator + config: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + namespace: aic + name: oas-validator-url-route +spec: + parentRefs: + - name: apisix + rules: + - matches: + - path: + type: PathPrefix + value: /api/v3/ + filters: + - type: ExtensionRef + extensionRef: + group: apisix.apache.org + kind: PluginConfig + name: oas-validator-url-plugin-config + backendRefs: + - name: httpbin-external-domain + port: 80 +``` + + + + + +```yaml title="oas-validator-url-ic.yaml" +apiVersion: apisix.apache.org/v2 +kind: ApisixUpstream +metadata: + namespace: aic + name: httpbin-external-domain +spec: + ingressClassName: apisix + externalNodes: + - type: Domain + name: httpbin.org +--- +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + namespace: aic + name: oas-validator-url-route +spec: + ingressClassName: apisix + http: + - name: oas-validator-url-route + match: + paths: + - /api/v3/* + upstreams: + - name: httpbin-external-domain + plugins: + - name: oas-validator + enable: true + config: + spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" + ssl_verify: false + verbose_errors: true +``` + + + + + +将配置应用到集群: + +```shell +kubectl apply -f oas-validator-url-ic.yaml +``` + + + + + +发送一个不符合 Petstore 规范的请求: + +```shell +curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ + -H "Content-Type: application/json" \ + -d '{"invalid": "body"}' +``` + +由于 `verbose_errors` 设置为 `true`,将收到包含详细校验错误信息的 `400` 响应。 diff --git a/t/plugin/oas-validator.t b/t/plugin/oas-validator.t new file mode 100644 index 000000000000..464d44108639 --- /dev/null +++ b/t/plugin/oas-validator.t @@ -0,0 +1,815 @@ +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local plugin = require("apisix.plugins.oas-validator") + local ospec = t.read_file("t/spec/spec.json") + + local ok, err = plugin.check_schema({spec = ospec}) + if not ok then + ngx.say(err) + end + + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: open api string should be json +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local ok, err = plugin.check_schema({spec = "invalid json string"}) + ngx.say(err) + } + } +--- response_body +invalid JSON string provided, err: Expected value but found invalid token at character 1 + + + +=== TEST 3: create route correctly +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 4: test body validation -- POST +--- request +POST /api/v3/pet +{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 5: test body validation -- PUT +--- request +PUT /api/v3/pet +{"id": 10, "name": "doggie", "category": { "id": 1, "name": "Dogs"}, "photoUrls": [ "string"], "tags": [{ "id": 0, "name": "string"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 6: passing incorrect body should fail +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 7: test body validation with Query Params +--- request +GET /api/v3/pet/findByStatus?status=pending +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 8: querying for married dogs should fail (incorrect query param) +--- request +GET /api/v3/pet/findByStatus?status=married +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 9: test body validation with Path Params +--- request +GET /api/v3/pet/10 +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 10: querying with wrong path uri param should fail +--- request +GET /api/v3/pet/wrong-id +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 11: create route for skipping body validation +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_request_body_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 12: passing incorrect body should pass validation (skip_request_body_validation = true) +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 13: create route for skipping header validation +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_request_header_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 14: passing incorrect header should pass validation (skip_request_header_validation = true) +--- request +GET /api/v3/pet/1 +--- more_headers +Content-Type: not-application/json +--- error_code: 200 + + + +=== TEST 15: create route for skipping query param validation +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_query_param_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 16: querying for incorrect query params should pass (skip_query_param_validation = true) +--- request +GET /api/v3/pet/findByStatus?status=married +--- more_headers +Content-Type: application/json +--- error_code: 200 + + + +=== TEST 17: create route for skipping path param validation +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_path_params_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 18: querying for incorrect path params should pass (skip_path_params_validation = true) +--- request +GET /api/v3/pet/incorrect-id +--- more_headers +Content-Type: application/json +--- error_code: 200 + + + +=== Test 19: test multipleOf validation +--- request +POST /api/v3/multipleoftest +{"testnumber": 1.13} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== Test 20: test multipleOf validation - invalid +--- request +POST /api/v3/multipleoftest +{"testnumber": 1.1312} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 21: route setup with reject_if_not_match = false +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 22: invalid body should still pass to upstream (reject_if_not_match is false) +--- upstream_server_config + location /api/v3/pet { + content_by_lua_block { + ngx.log(ngx.WARN, "upstream reached") + ngx.status = 200 + ngx.say("ok") + } + } +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- error_log +error occurred while validating request +--- grep_error_log eval +qr/upstream reached/ +--- grep_error_log_out +upstream reached + + + +=== TEST 23: create route with explicit reject_if_not_match = true +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 24: invalid body should be rejected when reject_if_not_match is true +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 25: create route with rejection_status_code = 422 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": true, + "rejection_status_code": 422 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 26: invalid body should return 422 when rejection_status_code = 422 +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 422 +--- error_log +error occurred while validating request + + + +=== TEST 27: create route with rejection_status_code = 503 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": true, + "rejection_status_code": 503 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 28: invalid body should return 503 when rejection_status_code = 503 +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 503 +--- error_log +error occurred while validating request + + + +=== TEST 29: schema should reject rejection_status_code = 399 (out of range) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "rejection_status_code": 399 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- error_code: 400 +--- response_body_like: validation failed + + + +=== TEST 30: schema should reject rejection_status_code = 600 (out of range) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "rejection_status_code": 600 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- error_code: 400 +--- response_body_like: validation failed + + + +=== TEST 31: boundary value rejection_status_code = 400 should be accepted +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "rejection_status_code": 400 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 32: boundary value rejection_status_code = 599 should be accepted +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "rejection_status_code": 599 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:6969": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 33: rejection_status_code should not affect behavior when reject_if_not_match = false +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": false, + "rejection_status_code": 422 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 34: invalid body should pass to upstream when reject_if_not_match = false (rejection_status_code ignored) +--- upstream_server_config + location /api/v3/pet { + content_by_lua_block { + ngx.log(ngx.WARN, "upstream reached") + ngx.status = 200 + ngx.say("ok") + } + } +--- request +POST /api/v3/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- error_log +error occurred while validating request +--- grep_error_log eval +qr/upstream reached/ +--- grep_error_log_out +upstream reached diff --git a/t/plugin/oas-validator2.t b/t/plugin/oas-validator2.t new file mode 100644 index 000000000000..3e359f6b5a16 --- /dev/null +++ b/t/plugin/oas-validator2.t @@ -0,0 +1,946 @@ +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + # fake server, only for test + server { + listen 1970; + location / { + content_by_lua_block { + ngx.say("ok") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: OAS 3.1 -- create route with spec31.json +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: OAS 3.1 exclusiveMinimum/Maximum (numeric) -- value within range should pass +--- request +POST /api/v31/exclusive +{"score": 50} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 3: OAS 3.1 exclusiveMinimum -- value equal to lower bound (0) should fail +--- request +POST /api/v31/exclusive +{"score": 0} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 4: OAS 3.1 exclusiveMaximum -- value equal to upper bound (100) should fail +--- request +POST /api/v31/exclusive +{"score": 100} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 5: OAS 3.1 if/then/else -- circle with radius should pass (then branch) +--- request +POST /api/v31/shape +{"type": "circle", "radius": 5} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 6: OAS 3.1 if/then -- circle without radius should fail +--- request +POST /api/v31/shape +{"type": "circle"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 7: OAS 3.1 if/else -- rectangle with width and height should pass (else branch) +--- request +POST /api/v31/shape +{"type": "rectangle", "width": 10, "height": 5} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 8: OAS 3.1 if/else -- rectangle missing width should fail +--- request +POST /api/v31/shape +{"type": "rectangle", "height": 5} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 9: OAS 3.1 anyOf -- matching first subschema should pass +--- request +POST /api/v31/anyof +{"name": "doggie"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 10: OAS 3.1 anyOf -- matching second subschema should pass +--- request +POST /api/v31/anyof +{"id": 42} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 11: OAS 3.1 anyOf -- matching neither subschema should fail +--- request +POST /api/v31/anyof +{"other": "value"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 12: OAS 3.1 oneOf -- matching exactly one subschema should pass +--- request +POST /api/v31/oneof +{"cat": "whiskers"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 13: OAS 3.1 oneOf -- matching both subschemas should fail +--- request +POST /api/v31/oneof +{"cat": "whiskers", "dog": "rex"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 14: OAS 3.1 allOf -- all subschemas satisfied should pass +--- request +POST /api/v31/allof +{"a": "hello", "b": 42} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 15: OAS 3.1 allOf -- missing field required by one subschema should fail +--- request +POST /api/v31/allof +{"a": "hello"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 16: OAS 3.1 -- route with spec31.json and reject_if_not_match = false +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": false + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 17: OAS 3.1 reject_if_not_match = false -- invalid body passes through to upstream +--- upstream_server_config + location /api/v31/pet { + content_by_lua_block { + ngx.log(ngx.WARN, "upstream reached") + ngx.status = 200 + ngx.say("ok") + } + } +--- request +POST /api/v31/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- error_log +error occurred while validating request +--- grep_error_log eval +qr/upstream reached/ +--- grep_error_log_out +upstream reached + + + +=== TEST 18: OAS 3.1 -- route with spec31.json and rejection_status_code = 422 +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "reject_if_not_match": true, + "rejection_status_code": 422 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 19: OAS 3.1 rejection_status_code = 422 -- invalid body returns 422 +--- request +POST /api/v31/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 422 +--- error_log +error occurred while validating request + + + +=== TEST 20: OAS 3.1 -- route with spec31.json and verbose_errors = true +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "verbose_errors": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 21: OAS 3.1 verbose_errors = true -- error response body contains schema detail +--- request +POST /api/v31/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request\..+ +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 22: OAS 3.1 -- route with spec31.json and skip_request_body_validation = true +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_request_body_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 23: OAS 3.1 skip_request_body_validation = true -- invalid body is not rejected +--- request +POST /api/v31/pet +{"lol": "watdis?"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 24: OAS 3.1 -- route with spec31.json and skip_query_param_validation = true +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_query_param_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 25: OAS 3.1 skip_query_param_validation = true -- invalid enum query param is not rejected +--- request +GET /api/v31/pet/findByStatus?status=married +--- more_headers +Content-Type: application/json +--- error_code: 200 + + + +=== TEST 26: OAS 3.1 -- route with spec31.json and skip_path_params_validation = true +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s", + "skip_path_params_validation": true + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + }, + "scheme": "http", + "pass_host": "pass" + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 27: OAS 3.1 skip_path_params_validation = true -- non-integer path param is not rejected +--- request +GET /api/v31/pet/not-an-id +--- more_headers +Content-Type: application/json +--- error_code: 200 + + + +=== TEST 28: OAS 3.1 -- restore route with spec31.json (no extra options) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 29: OAS 3.1 body validation -- valid Pet passes plugin (upstream returns 404) +--- request +POST /api/v31/pet +{"name": "doggie", "photoUrls": ["http://example.com/img.jpg"]} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 30: OAS 3.1 body validation -- missing required field should fail +--- request +POST /api/v31/pet +{"name": "doggie"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 31: OAS 3.1 path param validation -- valid integer id passes plugin +--- request +GET /api/v31/pet/42 +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 32: OAS 3.1 path param validation -- non-integer id should fail +--- request +GET /api/v31/pet/not-an-id +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 33: OAS 3.1 query param validation -- valid enum value passes plugin +--- request +GET /api/v31/pet/findByStatus?status=available +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 34: OAS 3.1 query param validation -- invalid enum value should fail +--- request +GET /api/v31/pet/findByStatus?status=married +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 35: OAS 3.1 nullable type array -- null value should pass +--- request +POST /api/v31/nullable +{"value": null} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 36: OAS 3.1 nullable type array -- string value should pass +--- request +POST /api/v31/nullable +{"value": "hello"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 37: OAS 3.1 nullable type array -- integer value should fail +--- request +POST /api/v31/nullable +{"value": 123} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 38: OAS 3.1 const keyword -- correct value should pass +--- request +POST /api/v31/const +{"version": "v1"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 39: OAS 3.1 const keyword -- wrong value should fail +--- request +POST /api/v31/const +{"version": "v2"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 40: OAS 3.1 multipleOf validation -- valid value passes +--- request +POST /api/v31/multipleoftest +{"testnumber": 1.13} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 41: OAS 3.1 multipleOf validation -- invalid value should fail +--- request +POST /api/v31/multipleoftest +{"testnumber": 1.1312} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 42: OAS 3.1 -- create route with spec31-gaps.json (components/pathItems, not, patternProperties, $dynamicRef) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local spec = t.read_file("t/spec/spec31-gaps.json") + spec = spec:gsub('\"', '\\"') + + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + string.format([[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec": "%s" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1970": 1 + } + } + }]], spec) + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 43: components/pathItems -- valid body via $ref path should pass +--- request +POST /api/v31gap/widget +{"name": "foo"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 44: components/pathItems -- invalid body via $ref path should fail +--- request +POST /api/v31gap/widget +{"notaname": "foo"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 45: not keyword -- value satisfying not constraint should pass +--- request +POST /api/v31gap/item +{"value": "not-an-integer"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 46: not keyword -- value violating not constraint should fail +--- request +POST /api/v31gap/item +{"value": 42} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 47: patternProperties -- matching key with correct type should pass +--- request +POST /api/v31gap/pattern +{"S_name": "hello"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 48: patternProperties -- matching key with wrong type should fail +--- request +POST /api/v31gap/pattern +{"S_name": 123} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 49: patternProperties with additionalProperties:false -- non-matching key should be rejected +--- request +POST /api/v31gap/pattern +{"S_name": "hello", "extra": "not_allowed"} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 50: $dynamicRef -- array with correct element type should pass +--- request +POST /api/v31gap/dynref +{"items": ["hello", "world"]} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 51: $dynamicRef -- array with wrong element type should fail +--- request +POST /api/v31gap/dynref +{"items": [1, 2, 3]} +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 52: [LIMITATION] contentMediaType/contentEncoding are annotations only -- non-JSON string passes without content validation +--- request +POST /api/v31gap/content-annotation +{"data": "this is NOT valid base64 nor JSON"} +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 53: prefixItems -- correct positional types should pass +--- request +POST /api/v31gap/prefixitems +["hello", 42, true] +--- more_headers +Content-Type: application/json +--- error_code: 200 +--- no_error_log +[error] + + + +=== TEST 54: prefixItems -- wrong type at first position should fail +--- request +POST /api/v31gap/prefixitems +[123, 42, true] +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request + + + +=== TEST 55: [LIMITATION] prefixItems + items -- extra items beyond prefixItems are validated by items schema +--- request +POST /api/v31gap/prefixitems +["hello", 42, "not_a_boolean"] +--- more_headers +Content-Type: application/json +--- response_body_like: failed to validate request. +--- error_code: 400 +--- error_log +error occurred while validating request From ff0d50bba2d6505440113ee765df76086d085535 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 09:13:53 +0800 Subject: [PATCH 02/13] fix: add license headers and remove trailing whitespace in oas-validator - Add Apache license headers to oas-validator.lua and test files - Remove trailing whitespace in t/plugin/oas-validator.t Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apisix/plugins/oas-validator.lua | 17 +++++++++++++++++ t/plugin/oas-validator.t | 20 ++++++++++++++++++-- t/plugin/oas-validator2.t | 16 ++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua index 6056aa0edc6b..b48d456d985e 100644 --- a/apisix/plugins/oas-validator.lua +++ b/apisix/plugins/oas-validator.lua @@ -1,3 +1,20 @@ +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You under the Apache License, Version 2.0 +-- (the "License"); you may not use this file except in compliance with +-- the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- + local core = require("apisix.core") local secret = require("apisix.secret") local plugin = require("apisix.plugin") diff --git a/t/plugin/oas-validator.t b/t/plugin/oas-validator.t index 464d44108639..9cb88fc1b8e0 100644 --- a/t/plugin/oas-validator.t +++ b/t/plugin/oas-validator.t @@ -1,3 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# use t::APISIX 'no_plan'; repeat_each(1); @@ -414,8 +430,8 @@ error occurred while validating request }, "upstream": { "type": "roundrobin", - "nodes": { - "127.0.0.1:1980": 1 + "nodes": { + "127.0.0.1:1980": 1 } } }]], spec) diff --git a/t/plugin/oas-validator2.t b/t/plugin/oas-validator2.t index 3e359f6b5a16..6e9804c6f5dc 100644 --- a/t/plugin/oas-validator2.t +++ b/t/plugin/oas-validator2.t @@ -1,3 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# use t::APISIX 'no_plan'; repeat_each(1); From d3a19796d191ef5a6eb03096c7fe0673d18eb06c Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 10:19:48 +0800 Subject: [PATCH 03/13] fix: add oas-validator to plugin list and include test spec files - Add oas-validator to expected plugin list in t/admin/plugins.t - Add OpenAPI spec fixture files (t/spec/*.json) required by tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- t/admin/plugins.t | 1 + t/spec/spec.json | 834 ++++++++++++++++++++++++++++++++++++++++ t/spec/spec31-gaps.json | 193 ++++++++++ t/spec/spec31.json | 300 +++++++++++++++ 4 files changed, 1328 insertions(+) create mode 100644 t/spec/spec.json create mode 100644 t/spec/spec31-gaps.json create mode 100644 t/spec/spec31.json diff --git a/t/admin/plugins.t b/t/admin/plugins.t index adb98b28bc17..f45256f177ba 100644 --- a/t/admin/plugins.t +++ b/t/admin/plugins.t @@ -115,6 +115,7 @@ gzip traffic-split redirect response-rewrite +oas-validator mcp-bridge degraphql kafka-proxy diff --git a/t/spec/spec.json b/t/spec/spec.json new file mode 100644 index 000000000000..c993fbfece88 --- /dev/null +++ b/t/spec/spec.json @@ -0,0 +1,834 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { "email": "apiteam@swagger.io" }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.17" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [{ "url": "/api/v3" }], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + }, + { "name": "user", "description": "Operations about user" } + ], + "paths": { + "/multipleoftest": { + "post": { + "tags": ["pet"], + "summary": "Test multipleOf validation", + "operationId": "testMultipleOf", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MultipleOfTest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { "type": "string" } + } + } + } + } + }, + "400": { "description": "Invalid input" } + } + } + }, + "/pet": { + "put": { + "tags": ["pet"], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" }, + "405": { "description": "Validation exception" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + }, + "post": { + "tags": ["pet"], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + } + }, + "405": { "description": "Invalid input" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/findByStatus": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": ["available", "pending", "sold"] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + } + } + }, + "400": { "description": "Invalid status value" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/findByTags": { + "get": { + "tags": ["pet"], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { "type": "array", "items": { "type": "string" } } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/Pet" } + } + } + } + }, + "400": { "description": "Invalid tag value" } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/{petId}": { + "get": { + "tags": ["pet"], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Pet not found" } + }, + "security": [ + { "api_key": [] }, + { "petstore_auth": ["write:pets", "read:pets"] } + ] + }, + "post": { + "tags": ["pet"], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { "type": "integer", "format": "int64" } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { "type": "string" } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { "type": "string" } + } + ], + "responses": { "405": { "description": "Invalid input" } }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + }, + "delete": { + "tags": ["pet"], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { "400": { "description": "Invalid pet value" } }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": ["pet"], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { "type": "integer", "format": "int64" } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { "type": "string" } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { "type": "string", "format": "binary" } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ApiResponse" } + } + } + } + }, + "security": [{ "petstore_auth": ["write:pets", "read:pets"] }] + } + }, + "/store/inventory": { + "get": { + "tags": ["store"], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [{ "api_key": [] }] + } + }, + "/store/order": { + "post": { + "tags": ["store"], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Order" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/Order" } + }, + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/Order" } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Order" } + } + } + }, + "405": { "description": "Invalid input" } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": ["store"], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/Order" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/Order" } + } + } + }, + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Order not found" } + } + }, + "delete": { + "tags": ["store"], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "400": { "description": "Invalid ID supplied" }, + "404": { "description": "Order not found" } + } + } + }, + "/user": { + "post": { + "tags": ["user"], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": ["user"], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + }, + "default": { "description": "successful operation" } + } + } + }, + "/user/login": { + "get": { + "tags": ["user"], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { "type": "string" } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { "type": "integer", "format": "int32" } + }, + "X-Expires-After": { + "description": "date in UTC when token expires", + "schema": { "type": "string", "format": "date-time" } + } + }, + "content": { + "application/xml": { "schema": { "type": "string" } }, + "application/json": { "schema": { "type": "string" } } + } + }, + "400": { "description": "Invalid username/password supplied" } + } + } + }, + "/user/logout": { + "get": { + "tags": ["user"], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/user/{username}": { + "get": { + "tags": ["user"], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + }, + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + }, + "put": { + "tags": ["user"], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/User" } + }, + "application/x-www-form-urlencoded": { + "schema": { "$ref": "#/components/schemas/User" } + } + } + }, + "responses": { "default": { "description": "successful operation" } } + }, + "delete": { + "tags": ["user"], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "400": { "description": "Invalid username supplied" }, + "404": { "description": "User not found" } + } + } + } + }, + "components": { + "schemas": { + "MultipleOfTest": { + "type": "object", + "required": ["testnumber"], + "properties": { + "testnumber": { + "type": "number", + "multipleOf": 0.01 + } + } + }, + "Order": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "petId": { "type": "integer", "format": "int64", "example": 198772 }, + "quantity": { "type": "integer", "format": "int32", "example": 7 }, + "shipDate": { "type": "string", "format": "date-time" }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": ["placed", "approved", "delivered"] + }, + "complete": { "type": "boolean" } + }, + "xml": { "name": "order" } + }, + "Customer": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 100000 }, + "username": { "type": "string", "example": "fehguy" }, + "address": { + "type": "array", + "xml": { "name": "addresses", "wrapped": true }, + "items": { "$ref": "#/components/schemas/Address" } + } + }, + "xml": { "name": "customer" } + }, + "Address": { + "type": "object", + "properties": { + "street": { "type": "string", "example": "437 Lytton" }, + "city": { "type": "string", "example": "Palo Alto" }, + "state": { "type": "string", "example": "CA" }, + "zip": { "type": "string", "example": "94301" } + }, + "xml": { "name": "address" } + }, + "Category": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 1 }, + "name": { "type": "string", "example": "Dogs" } + }, + "xml": { "name": "category" } + }, + "User": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "username": { "type": "string", "example": "theUser" }, + "firstName": { "type": "string", "example": "John" }, + "lastName": { "type": "string", "example": "James" }, + "email": { "type": "string", "example": "john@email.com" }, + "password": { "type": "string", "example": "12345" }, + "phone": { "type": "string", "example": "12345" }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { "name": "user" } + }, + "Tag": { + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64" }, + "name": { "type": "string" } + }, + "xml": { "name": "tag" } + }, + "Pet": { + "required": ["name", "photoUrls"], + "type": "object", + "properties": { + "id": { "type": "integer", "format": "int64", "example": 10 }, + "name": { "type": "string", "example": "doggie" }, + "category": { "$ref": "#/components/schemas/Category" }, + "photoUrls": { + "type": "array", + "xml": { "wrapped": true }, + "items": { "type": "string", "xml": { "name": "photoUrl" } } + }, + "tags": { + "type": "array", + "xml": { "wrapped": true }, + "items": { "$ref": "#/components/schemas/Tag" } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": ["available", "pending", "sold"] + } + }, + "xml": { "name": "pet" } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { "type": "integer", "format": "int32" }, + "type": { "type": "string" }, + "message": { "type": "string" } + }, + "xml": { "name": "##default" } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + }, + "application/xml": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } + } + } +} diff --git a/t/spec/spec31-gaps.json b/t/spec/spec31-gaps.json new file mode 100644 index 000000000000..39364eb19bc8 --- /dev/null +++ b/t/spec/spec31-gaps.json @@ -0,0 +1,193 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OAS 3.1 Gap Tests", + "version": "1.0.0" + }, + "servers": [{ "url": "/api/v31gap" }], + "paths": { + "/widget": { + "$ref": "#/components/pathItems/WidgetPath" + }, + "/widget-inline": { + "post": { + "operationId": "createWidgetInline", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Widget" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/item": { + "post": { + "operationId": "createItem", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ItemNotTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/pattern": { + "post": { + "operationId": "createPattern", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PatternTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/dynref": { + "post": { + "operationId": "createDynref", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/DynRefTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/content-annotation": { + "post": { + "operationId": "createContentAnnotation", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ContentAnnotationTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/prefixitems": { + "post": { + "operationId": "createPrefixItems", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/PrefixItemsTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + } + }, + "components": { + "pathItems": { + "WidgetPath": { + "post": { + "operationId": "createWidget", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Widget" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + } + }, + "schemas": { + "Widget": { + "type": "object", + "required": ["name"], + "properties": { + "name": { "type": "string" } + } + }, + "ItemNotTest": { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "not": { "type": "integer" }, + "description": "must not be an integer" + } + } + }, + "PatternTest": { + "type": "object", + "patternProperties": { + "^S_": { "type": "string" }, + "^I_": { "type": "integer" } + }, + "additionalProperties": false + }, + "DynRefTest": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { "$dynamicRef": "#items" } + } + }, + "$defs": { + "defaultItem": { + "$dynamicAnchor": "items", + "type": "string" + } + } + }, + "ContentAnnotationTest": { + "type": "object", + "required": ["data"], + "properties": { + "data": { + "type": "string", + "contentMediaType": "application/json", + "contentEncoding": "base64", + "description": "content* keywords are annotations only; any string passes" + } + } + }, + "PrefixItemsTest": { + "type": "array", + "prefixItems": [ + { "type": "string" }, + { "type": "integer" } + ], + "items": { "type": "boolean" }, + "description": "first two items: string then integer; additional items must be boolean" + } + } + } +} diff --git a/t/spec/spec31.json b/t/spec/spec31.json new file mode 100644 index 000000000000..c46b78a0040f --- /dev/null +++ b/t/spec/spec31.json @@ -0,0 +1,300 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Pet Store - OpenAPI 3.1", + "version": "1.0.0" + }, + "servers": [{ "url": "/api/v31" }], + "paths": { + "/pet": { + "post": { + "operationId": "addPet31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Pet" } + } + } + }, + "responses": { + "200": { "description": "successful operation" }, + "400": { "description": "Invalid input" } + } + } + }, + "/pet/{petId}": { + "get": { + "operationId": "getPet31", + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { "description": "successful operation" }, + "400": { "description": "Invalid ID" } + } + } + }, + "/pet/findByStatus": { + "get": { + "operationId": "findPetsByStatus31", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": ["available", "pending", "sold"] + } + } + ], + "responses": { + "200": { "description": "successful operation" }, + "400": { "description": "Invalid status" } + } + } + }, + "/nullable": { + "post": { + "operationId": "nullableTest", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/NullableTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/const": { + "post": { + "operationId": "constTest", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ConstTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/multipleoftest": { + "post": { + "operationId": "multipleOfTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/MultipleOfTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/exclusive": { + "post": { + "operationId": "exclusiveTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ExclusiveTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/shape": { + "post": { + "operationId": "shapeTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/Shape" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/anyof": { + "post": { + "operationId": "anyOfTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AnyOfTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/oneof": { + "post": { + "operationId": "oneOfTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/OneOfTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + }, + "/allof": { + "post": { + "operationId": "allOfTest31", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/AllOfTest" } + } + } + }, + "responses": { + "200": { "description": "ok" } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["name", "photoUrls"], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "photoUrls": { + "type": "array", + "items": { "type": "string" } + }, + "status": { + "type": "string", + "enum": ["available", "pending", "sold"] + }, + "tag": { "$ref": "#/components/schemas/Tag" } + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" } + } + }, + "NullableTest": { + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": ["string", "null"], + "description": "OAS 3.1 nullable via type array" + } + } + }, + "ConstTest": { + "type": "object", + "required": ["version"], + "properties": { + "version": { + "const": "v1", + "description": "OAS 3.1 const keyword" + } + } + }, + "MultipleOfTest": { + "type": "object", + "required": ["testnumber"], + "properties": { + "testnumber": { + "type": "number", + "multipleOf": 0.01 + } + } + }, + "ExclusiveTest": { + "type": "object", + "required": ["score"], + "properties": { + "score": { + "type": "number", + "exclusiveMinimum": 0, + "exclusiveMaximum": 100 + } + } + }, + "Shape": { + "type": "object", + "required": ["type"], + "properties": { + "type": { "type": "string" } + }, + "if": { + "properties": { "type": { "const": "circle" } } + }, + "then": { + "required": ["radius"], + "properties": { "radius": { "type": "number" } } + }, + "else": { + "required": ["width", "height"], + "properties": { + "width": { "type": "number" }, + "height": { "type": "number" } + } + } + }, + "AnyOfTest": { + "anyOf": [ + { "type": "object", "required": ["name"], "properties": { "name": { "type": "string" } } }, + { "type": "object", "required": ["id"], "properties": { "id": { "type": "integer" } } } + ] + }, + "OneOfTest": { + "oneOf": [ + { "type": "object", "required": ["cat"], "properties": { "cat": { "type": "string" } } }, + { "type": "object", "required": ["dog"], "properties": { "dog": { "type": "string" } } } + ] + }, + "AllOfTest": { + "allOf": [ + { "type": "object", "required": ["a"], "properties": { "a": { "type": "string" } } }, + { "type": "object", "required": ["b"], "properties": { "b": { "type": "integer" } } } + ] + } + } + } +} From 75279ce4b5f425792fe9946e4699c8ee9355ed0b Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 11:29:36 +0800 Subject: [PATCH 04/13] fix(oas-validator): fix priority conflict and test upstream addresses - Change priority from 510 to 512 to avoid conflict with mcp-bridge - Move plugin registration position in config.lua accordingly - Add fake upstream server on port 1971 in test preprocessor - Replace unreachable port 6969/1980 with port 1971 in test upstreams - Fix oas-validator2.t: replace 1980 with 1970 for TEST 22/24/26 - Remove trailing scheme/pass_host fields from updated upstream blocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- apisix/cli/config.lua | 2 +- apisix/plugins/oas-validator.lua | 2 +- t/plugin/oas-validator.t | 55 ++++++++++++++++++-------------- t/plugin/oas-validator2.t | 18 ++++------- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua index 52be5b0ed50b..2af80eb352c8 100644 --- a/apisix/cli/config.lua +++ b/apisix/cli/config.lua @@ -244,13 +244,13 @@ local _M = { "limit-conn", "limit-count", "limit-req", - "oas-validator", "gzip", -- deprecated and will be removed in a future release -- "server-info", "traffic-split", "redirect", "response-rewrite", + "oas-validator", "mcp-bridge", "degraphql", "kafka-proxy", diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua index b48d456d985e..791c0303d321 100644 --- a/apisix/plugins/oas-validator.lua +++ b/apisix/plugins/oas-validator.lua @@ -171,7 +171,7 @@ end local _M = { version = 0.1, - priority = 510, + priority = 512, name = plugin_name, schema = schema, metadata_schema = metadata_schema, diff --git a/t/plugin/oas-validator.t b/t/plugin/oas-validator.t index 9cb88fc1b8e0..11539a3f34da 100644 --- a/t/plugin/oas-validator.t +++ b/t/plugin/oas-validator.t @@ -24,6 +24,21 @@ no_root_location(); add_block_preprocessor(sub { my ($block) = @_; + my $http_config = $block->http_config // <<_EOC_; + # fake upstream server for pass-through validation tests + server { + listen 1971; + location / { + content_by_lua_block { + ngx.status = 200 + ngx.say("ok") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + if (!$block->request) { $block->set_value("request", "GET /t"); } @@ -92,7 +107,7 @@ invalid JSON string provided, err: Expected value but found invalid token at cha "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -209,10 +224,8 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1971": 1 + } } }]], spec) ) @@ -260,10 +273,8 @@ Content-Type: application/json "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1971": 1 + } } }]], spec) ) @@ -309,10 +320,8 @@ Content-Type: not-application/json "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1971": 1 + } } }]], spec) ) @@ -358,10 +367,8 @@ Content-Type: application/json "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1971": 1 + } } }]], spec) ) @@ -491,7 +498,7 @@ upstream reached "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -542,7 +549,7 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -593,7 +600,7 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -643,7 +650,7 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -680,7 +687,7 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -717,7 +724,7 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) @@ -754,7 +761,7 @@ passed "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:6969": 1 + "127.0.0.1:1971": 1 } } }]], spec) diff --git a/t/plugin/oas-validator2.t b/t/plugin/oas-validator2.t index 6e9804c6f5dc..f3bc151d4bf0 100644 --- a/t/plugin/oas-validator2.t +++ b/t/plugin/oas-validator2.t @@ -445,10 +445,8 @@ error occurred while validating request "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1970": 1 + } } }]], spec) ) @@ -495,10 +493,8 @@ Content-Type: application/json "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1970": 1 + } } }]], spec) ) @@ -543,10 +539,8 @@ Content-Type: application/json "upstream": { "type": "roundrobin", "nodes": { - "127.0.0.1:1980": 1 - }, - "scheme": "http", - "pass_host": "pass" + "127.0.0.1:1970": 1 + } } }]], spec) ) From f15517d33f7342d214ef6491ae54a5d40024a3b7 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 12:04:15 +0800 Subject: [PATCH 05/13] docs(oas-validator): fix documentation style to match APISIX conventions - Rename APISIX CRD tab to APISIX Ingress Controller - Simplify admin_key note block Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/en/latest/plugins/oas-validator.md | 10 ++++------ docs/zh/latest/plugins/oas-validator.md | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/en/latest/plugins/oas-validator.md b/docs/en/latest/plugins/oas-validator.md index c37ebd1c2c6a..135305286746 100644 --- a/docs/en/latest/plugins/oas-validator.md +++ b/docs/en/latest/plugins/oas-validator.md @@ -75,8 +75,6 @@ The examples below demonstrate how you can configure `oas-validator` in differen :::note -You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: - ```bash admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') ``` @@ -155,7 +153,7 @@ groupId="k8s-api" defaultValue="gateway-api" values={[ {label: 'Gateway API', value: 'gateway-api'}, -{label: 'APISIX CRD', value: 'apisix-crd'} +{label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} ]}> @@ -208,7 +206,7 @@ spec: - + ```yaml title="oas-validator-ic.yaml" apiVersion: apisix.apache.org/v2 @@ -364,7 +362,7 @@ groupId="k8s-api" defaultValue="gateway-api" values={[ {label: 'Gateway API', value: 'gateway-api'}, -{label: 'APISIX CRD', value: 'apisix-crd'} +{label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} ]}> @@ -418,7 +416,7 @@ spec: - + ```yaml title="oas-validator-url-ic.yaml" apiVersion: apisix.apache.org/v2 diff --git a/docs/zh/latest/plugins/oas-validator.md b/docs/zh/latest/plugins/oas-validator.md index 37e69e2ae78f..4b2e16daa517 100644 --- a/docs/zh/latest/plugins/oas-validator.md +++ b/docs/zh/latest/plugins/oas-validator.md @@ -75,8 +75,6 @@ OpenAPI 规范可以以内联 JSON 字符串的形式提供,也可以从远程 :::note -你可以这样从 `config.yaml` 中获取 `admin_key` 并存入环境变量: - ```bash admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g') ``` @@ -155,7 +153,7 @@ groupId="k8s-api" defaultValue="gateway-api" values={[ {label: 'Gateway API', value: 'gateway-api'}, -{label: 'APISIX CRD', value: 'apisix-crd'} +{label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} ]}> @@ -208,7 +206,7 @@ spec: - + ```yaml title="oas-validator-ic.yaml" apiVersion: apisix.apache.org/v2 @@ -364,7 +362,7 @@ groupId="k8s-api" defaultValue="gateway-api" values={[ {label: 'Gateway API', value: 'gateway-api'}, -{label: 'APISIX CRD', value: 'apisix-crd'} +{label: 'APISIX Ingress Controller', value: 'apisix-ingress-controller'} ]}> @@ -418,7 +416,7 @@ spec: - + ```yaml title="oas-validator-url-ic.yaml" apiVersion: apisix.apache.org/v2 From 4f393ee3c31b74c63c489d992f08dc65bc5a28b9 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 12:34:39 +0800 Subject: [PATCH 06/13] docs(oas-validator): remove ADC/IC tabs, keep Admin API examples only Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/en/latest/plugins/oas-validator.md | 310 ------------------------ docs/zh/latest/plugins/oas-validator.md | 310 ------------------------ 2 files changed, 620 deletions(-) diff --git a/docs/en/latest/plugins/oas-validator.md b/docs/en/latest/plugins/oas-validator.md index 135305286746..a96fe2c3e0d5 100644 --- a/docs/en/latest/plugins/oas-validator.md +++ b/docs/en/latest/plugins/oas-validator.md @@ -33,9 +33,6 @@ description: The oas-validator Plugin validates incoming HTTP requests against a -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## Description The `oas-validator` Plugin validates incoming HTTP requests against an [OpenAPI Specification (OAS) 3.x](https://swagger.io/specification/) document before forwarding them to the upstream service. It can validate the request method, path, query parameters, request headers, and body. @@ -85,17 +82,6 @@ admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"/ The following example demonstrates how to validate requests against an inline OpenAPI 3.x specification. Requests that do not conform to the spec are rejected with a `400` response. - - - - ```shell curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ -H "X-API-KEY: ${admin_key}" \ @@ -115,147 +101,6 @@ curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ }' ``` - - - - -```yaml title="adc.yaml" -services: - - name: httpbin - routes: - - name: oas-validator-route - uris: - - /api/v3/* - plugins: - oas-validator: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true - upstream: - type: roundrobin - nodes: - - host: httpbin.org - port: 80 - weight: 1 -``` - -Synchronize the configuration to the gateway: - -```shell -adc sync -f adc.yaml -``` - - - - - - - - - -```yaml title="oas-validator-ic.yaml" -apiVersion: v1 -kind: Service -metadata: - namespace: aic - name: httpbin-external-domain -spec: - type: ExternalName - externalName: httpbin.org ---- -apiVersion: apisix.apache.org/v1alpha1 -kind: PluginConfig -metadata: - namespace: aic - name: oas-validator-plugin-config -spec: - plugins: - - name: oas-validator - config: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - namespace: aic - name: oas-validator-route -spec: - parentRefs: - - name: apisix - rules: - - matches: - - path: - type: PathPrefix - value: /api/v3/ - filters: - - type: ExtensionRef - extensionRef: - group: apisix.apache.org - kind: PluginConfig - name: oas-validator-plugin-config - backendRefs: - - name: httpbin-external-domain - port: 80 -``` - - - - - -```yaml title="oas-validator-ic.yaml" -apiVersion: apisix.apache.org/v2 -kind: ApisixUpstream -metadata: - namespace: aic - name: httpbin-external-domain -spec: - ingressClassName: apisix - externalNodes: - - type: Domain - name: httpbin.org ---- -apiVersion: apisix.apache.org/v2 -kind: ApisixRoute -metadata: - namespace: aic - name: oas-validator-route -spec: - ingressClassName: apisix - http: - - name: oas-validator-route - match: - paths: - - /api/v3/* - upstreams: - - name: httpbin-external-domain - plugins: - - name: oas-validator - enable: true - config: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true -``` - - - - - -Apply the configuration to your cluster: - -```shell -kubectl apply -f oas-validator-ic.yaml -``` - - - - - Send a valid request with the required `name` field: ```shell @@ -280,17 +125,6 @@ You should receive a `400` response with a validation error message. The following example demonstrates how to fetch the OpenAPI specification from a remote URL. The spec is fetched once and cached for the duration specified by `spec_url_ttl` in the plugin metadata. - - - - Configure the plugin metadata to set the cache TTL for the remote spec: ```shell @@ -323,150 +157,6 @@ curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ }' ``` - - - - -```yaml title="adc.yaml" -services: - - name: httpbin - routes: - - name: oas-validator-url-route - uris: - - /api/v3/* - plugins: - oas-validator: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true - upstream: - type: roundrobin - nodes: - - host: httpbin.org - port: 80 - weight: 1 -``` - -Synchronize the configuration to the gateway: - -```shell -adc sync -f adc.yaml -``` - - - - - - - - - -```yaml title="oas-validator-url-ic.yaml" -apiVersion: v1 -kind: Service -metadata: - namespace: aic - name: httpbin-external-domain -spec: - type: ExternalName - externalName: httpbin.org ---- -apiVersion: apisix.apache.org/v1alpha1 -kind: PluginConfig -metadata: - namespace: aic - name: oas-validator-url-plugin-config -spec: - plugins: - - name: oas-validator - config: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - namespace: aic - name: oas-validator-url-route -spec: - parentRefs: - - name: apisix - rules: - - matches: - - path: - type: PathPrefix - value: /api/v3/ - filters: - - type: ExtensionRef - extensionRef: - group: apisix.apache.org - kind: PluginConfig - name: oas-validator-url-plugin-config - backendRefs: - - name: httpbin-external-domain - port: 80 -``` - - - - - -```yaml title="oas-validator-url-ic.yaml" -apiVersion: apisix.apache.org/v2 -kind: ApisixUpstream -metadata: - namespace: aic - name: httpbin-external-domain -spec: - ingressClassName: apisix - externalNodes: - - type: Domain - name: httpbin.org ---- -apiVersion: apisix.apache.org/v2 -kind: ApisixRoute -metadata: - namespace: aic - name: oas-validator-url-route -spec: - ingressClassName: apisix - http: - - name: oas-validator-url-route - match: - paths: - - /api/v3/* - upstreams: - - name: httpbin-external-domain - plugins: - - name: oas-validator - enable: true - config: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true -``` - - - - - -Apply the configuration to your cluster: - -```shell -kubectl apply -f oas-validator-url-ic.yaml -``` - - - - - Send a request that does not conform to the Petstore spec: ```shell diff --git a/docs/zh/latest/plugins/oas-validator.md b/docs/zh/latest/plugins/oas-validator.md index 4b2e16daa517..0dc3e9585024 100644 --- a/docs/zh/latest/plugins/oas-validator.md +++ b/docs/zh/latest/plugins/oas-validator.md @@ -33,9 +33,6 @@ description: oas-validator 插件根据 OpenAPI Specification(OAS)3.x 文档 -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ## 描述 `oas-validator` 插件在请求转发至上游服务之前,根据 [OpenAPI Specification(OAS)3.x](https://swagger.io/specification/) 文档对入站 HTTP 请求进行校验。可校验内容包括请求方法、路径、查询参数、请求头以及请求体。 @@ -85,17 +82,6 @@ admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"/ 以下示例演示如何使用内联 OpenAPI 3.x 规范校验请求。不符合规范的请求将以 `400` 响应被拒绝。 - - - - ```shell curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ -H "X-API-KEY: ${admin_key}" \ @@ -115,147 +101,6 @@ curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ }' ``` - - - - -```yaml title="adc.yaml" -services: - - name: httpbin - routes: - - name: oas-validator-route - uris: - - /api/v3/* - plugins: - oas-validator: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true - upstream: - type: roundrobin - nodes: - - host: httpbin.org - port: 80 - weight: 1 -``` - -将配置同步到网关: - -```shell -adc sync -f adc.yaml -``` - - - - - - - - - -```yaml title="oas-validator-ic.yaml" -apiVersion: v1 -kind: Service -metadata: - namespace: aic - name: httpbin-external-domain -spec: - type: ExternalName - externalName: httpbin.org ---- -apiVersion: apisix.apache.org/v1alpha1 -kind: PluginConfig -metadata: - namespace: aic - name: oas-validator-plugin-config -spec: - plugins: - - name: oas-validator - config: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - namespace: aic - name: oas-validator-route -spec: - parentRefs: - - name: apisix - rules: - - matches: - - path: - type: PathPrefix - value: /api/v3/ - filters: - - type: ExtensionRef - extensionRef: - group: apisix.apache.org - kind: PluginConfig - name: oas-validator-plugin-config - backendRefs: - - name: httpbin-external-domain - port: 80 -``` - - - - - -```yaml title="oas-validator-ic.yaml" -apiVersion: apisix.apache.org/v2 -kind: ApisixUpstream -metadata: - namespace: aic - name: httpbin-external-domain -spec: - ingressClassName: apisix - externalNodes: - - type: Domain - name: httpbin.org ---- -apiVersion: apisix.apache.org/v2 -kind: ApisixRoute -metadata: - namespace: aic - name: oas-validator-route -spec: - ingressClassName: apisix - http: - - name: oas-validator-route - match: - paths: - - /api/v3/* - upstreams: - - name: httpbin-external-domain - plugins: - - name: oas-validator - enable: true - config: - spec: '{"openapi":"3.0.2","info":{"title":"Pet API","version":"1.0.0"},"paths":{"/api/v3/pet":{"post":{"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"status":{"type":"string"}}}}}},"responses":{"200":{"description":"OK"}}}}}}' - verbose_errors: true -``` - - - - - -将配置应用到集群: - -```shell -kubectl apply -f oas-validator-ic.yaml -``` - - - - - 发送一个包含必填 `name` 字段的合法请求: ```shell @@ -280,17 +125,6 @@ curl -i "http://127.0.0.1:9080/api/v3/pet" -X POST \ 以下示例演示如何从远程 URL 获取 OpenAPI 规范。规范在首次获取后会被缓存,缓存时长由插件元数据的 `spec_url_ttl` 参数决定。 - - - - 配置插件元数据以设置远程规范的缓存时间: ```shell @@ -323,150 +157,6 @@ curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \ }' ``` - - - - -```yaml title="adc.yaml" -services: - - name: httpbin - routes: - - name: oas-validator-url-route - uris: - - /api/v3/* - plugins: - oas-validator: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true - upstream: - type: roundrobin - nodes: - - host: httpbin.org - port: 80 - weight: 1 -``` - -将配置同步到网关: - -```shell -adc sync -f adc.yaml -``` - - - - - - - - - -```yaml title="oas-validator-url-ic.yaml" -apiVersion: v1 -kind: Service -metadata: - namespace: aic - name: httpbin-external-domain -spec: - type: ExternalName - externalName: httpbin.org ---- -apiVersion: apisix.apache.org/v1alpha1 -kind: PluginConfig -metadata: - namespace: aic - name: oas-validator-url-plugin-config -spec: - plugins: - - name: oas-validator - config: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - namespace: aic - name: oas-validator-url-route -spec: - parentRefs: - - name: apisix - rules: - - matches: - - path: - type: PathPrefix - value: /api/v3/ - filters: - - type: ExtensionRef - extensionRef: - group: apisix.apache.org - kind: PluginConfig - name: oas-validator-url-plugin-config - backendRefs: - - name: httpbin-external-domain - port: 80 -``` - - - - - -```yaml title="oas-validator-url-ic.yaml" -apiVersion: apisix.apache.org/v2 -kind: ApisixUpstream -metadata: - namespace: aic - name: httpbin-external-domain -spec: - ingressClassName: apisix - externalNodes: - - type: Domain - name: httpbin.org ---- -apiVersion: apisix.apache.org/v2 -kind: ApisixRoute -metadata: - namespace: aic - name: oas-validator-url-route -spec: - ingressClassName: apisix - http: - - name: oas-validator-url-route - match: - paths: - - /api/v3/* - upstreams: - - name: httpbin-external-domain - plugins: - - name: oas-validator - enable: true - config: - spec_url: "https://petstore3.swagger.io/api/v3/openapi.json" - ssl_verify: false - verbose_errors: true -``` - - - - - -将配置应用到集群: - -```shell -kubectl apply -f oas-validator-url-ic.yaml -``` - - - - - 发送一个不符合 Petstore 规范的请求: ```shell From f6330e10edc59ce53c7df9e5a536749af05aa3c3 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 13:57:23 +0800 Subject: [PATCH 07/13] test(oas-validator): add tests for spec_url feature Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- t/plugin/oas-validator3.t | 548 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 548 insertions(+) create mode 100644 t/plugin/oas-validator3.t diff --git a/t/plugin/oas-validator3.t b/t/plugin/oas-validator3.t new file mode 100644 index 000000000000..1026298c65b6 --- /dev/null +++ b/t/plugin/oas-validator3.t @@ -0,0 +1,548 @@ +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + +add_block_preprocessor(sub { + my ($block) = @_; + + my $http_config = $block->http_config // <<_EOC_; + server { + listen 1979; + location /spec.json { + content_by_lua_block { + local file = io.open("t/spec/spec.json", "r") + local content = file:read("*a") + file:close() + ngx.print(content) + } + } + location /invalid.json { + content_by_lua_block { + ngx.print("not valid json {{{") + } + } + location /not-found.json { + content_by_lua_block { + ngx.status = 404 + ngx.print("not found") + } + } + location /spec-with-auth.json { + content_by_lua_block { + local headers = ngx.req.get_headers() + if headers["X-Token"] ~= "my-secret-token" then + ngx.status = 403 + ngx.print("forbidden") + return + end + local file = io.open("t/spec/spec.json", "r") + local content = file:read("*a") + file:close() + ngx.print(content) + } + } + } + + server { + listen 1970; + location / { + content_by_lua_block { + ngx.say("ok") + } + } + } +_EOC_ + + $block->set_value("http_config", $http_config); + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->error_log && !$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: schema validation -- spec_url is accepted +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local ok, err = plugin.check_schema({ + spec_url = "http://127.0.0.1:1979/spec.json" + }) + if not ok then + ngx.say(err) + return + end + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 2: schema validation -- spec and spec_url are mutually exclusive +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local ok, err = plugin.check_schema({ + spec = "{}", + spec_url = "http://127.0.0.1:1979/spec.json" + }) + if not ok then + ngx.say("rejected") + return + end + ngx.say("ok") + } + } +--- response_body +rejected + + + +=== TEST 3: schema validation -- neither spec nor spec_url fails +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local ok, err = plugin.check_schema({ + verbose_errors = true + }) + if not ok then + ngx.say("rejected") + return + end + ngx.say("ok") + } + } +--- response_body +rejected + + + +=== TEST 4: schema validation -- spec_url must be http/https +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local ok, err = plugin.check_schema({ + spec_url = "ftp://example.com/spec.json" + }) + if not ok then + ngx.say("rejected") + return + end + ngx.say("ok") + } + } +--- response_body +rejected + + + +=== TEST 5: create route with spec_url +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/spec.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 6: request validation works with spec_url +--- request +POST /api/v3/pet +{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 7: invalid request body fails validation with spec_url +--- request +POST /api/v3/pet +{"invalid": "body"} +--- more_headers +Content-Type: application/json +--- error_code: 400 +--- response_body_like: failed to validate request +--- error_log +error occurred while validating request + + + +=== TEST 8: spec_url returning non-200 triggers error +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/not-found.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 9: request to route with non-200 spec_url returns 500 +--- request +GET /api/v3/pet/1 +--- error_code: 500 +--- response_body_like: failed to parse openapi spec +--- error_log +spec URL returned status 404 + + + +=== TEST 10: spec_url returning invalid JSON triggers error +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/invalid.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 11: request to route with invalid JSON spec_url returns 500 +--- request +GET /api/v3/pet/1 +--- error_code: 500 +--- response_body_like: failed to parse openapi spec +--- error_log +failed to compile openapi spec fetched from URL + + + +=== TEST 12: spec_url with custom request headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/spec-with-auth.json", + "spec_url_request_headers": { + "X-Token": "my-secret-token" + } + } + }, + "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 13: request validation works with custom headers spec_url +--- request +POST /api/v3/pet +{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 14: spec_url without required auth header fails +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/spec-with-auth.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 15: request to route with missing auth returns 500 +--- request +GET /api/v3/pet/1 +--- error_code: 500 +--- response_body_like: failed to parse openapi spec +--- error_log +spec URL returned status 403 + + + +=== TEST 16: metadata schema validation +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local core = require("apisix.core") + local ok, err = plugin.check_schema({spec_url_ttl = 60}, core.schema.TYPE_METADATA) + if not ok then + ngx.say(err) + return + end + ngx.say("done") + } + } +--- response_body +done + + + +=== TEST 17: metadata schema rejects invalid ttl +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.oas-validator") + local core = require("apisix.core") + local ok, err = plugin.check_schema({spec_url_ttl = 0}, core.schema.TYPE_METADATA) + if not ok then + ngx.say("rejected") + return + end + ngx.say("ok") + } + } +--- response_body +rejected + + + +=== TEST 18: set plugin metadata with custom TTL +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/plugin_metadata/oas-validator', + ngx.HTTP_PUT, + [[{ + "spec_url_ttl": 2 + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 19: create route with spec_url for TTL test +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/*", + "plugins": { + "oas-validator": { + "spec_url": "http://127.0.0.1:1979/spec.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 20: first request fetches and caches spec +--- request +POST /api/v3/pet +{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 21: second request uses cached spec (no refetch) +--- request +POST /api/v3/pet +{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"} +--- more_headers +Content-Type: application/json +--- no_error_log +[error] + + + +=== TEST 22: after TTL expiry, stale validator still works (async refresh) +--- config + location /t { + content_by_lua_block { + ngx.sleep(3) + local http = require("resty.http") + local httpc = http.new() + local res, err = httpc:request_uri("http://127.0.0.1:" .. ngx.var.server_port .. "/api/v3/pet", { + method = "POST", + body = '{"id": 10, "name": "doggie", "category": {"id": 1, "name": "Dogs"}, "photoUrls": ["string"], "tags": [{"id": 1, "name": "tag1"}], "status": "available"}', + headers = { + ["Content-Type"] = "application/json", + } + }) + if not res then + ngx.say("request failed: " .. err) + return + end + ngx.say("status: " .. res.status) + } + } +--- response_body +status: 200 +--- no_error_log +[error] + + + +=== TEST 23: clean up metadata +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/plugin_metadata/oas-validator', + ngx.HTTP_DELETE + ) + ngx.say(body) + } + } +--- response_body +passed From 6e3e4c908f914640e56f24420accca56a95434e2 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 14:02:51 +0800 Subject: [PATCH 08/13] test(oas-validator): add Apache license header to oas-validator3.t Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- t/plugin/oas-validator3.t | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/t/plugin/oas-validator3.t b/t/plugin/oas-validator3.t index 1026298c65b6..cdd2b90104e9 100644 --- a/t/plugin/oas-validator3.t +++ b/t/plugin/oas-validator3.t @@ -1,3 +1,19 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# use t::APISIX 'no_plan'; repeat_each(1); From 3b0de704da3c8ec451b8291f69d585d63e714db8 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 14:23:16 +0800 Subject: [PATCH 09/13] feat(oas-validator): add oas-validator to config.yaml.example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- conf/config.yaml.example | 1 + 1 file changed, 1 insertion(+) diff --git a/conf/config.yaml.example b/conf/config.yaml.example index ae7155a86b06..0e068d542daa 100644 --- a/conf/config.yaml.example +++ b/conf/config.yaml.example @@ -533,6 +533,7 @@ plugins: # plugin list (sorted by priority) - traffic-split # priority: 966 - redirect # priority: 900 - response-rewrite # priority: 899 + - oas-validator # priority: 512 - mcp-bridge # priority: 510 - degraphql # priority: 509 - kafka-proxy # priority: 508 From 633d26c6d2ae2eb4c99606c4bc1a85b9a7e62490 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 17:38:27 +0800 Subject: [PATCH 10/13] docs: add oas-validator to config.json sidebar --- docs/en/latest/config.json | 1 + docs/zh/latest/config.json | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index e5459e5bdfc5..52d018afbcd4 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -163,6 +163,7 @@ "plugins/proxy-cache", "plugins/request-validation", "plugins/oas-validator", + "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 94733997389b..3da3f8a1cc63 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -152,6 +152,7 @@ "plugins/proxy-cache", "plugins/request-validation", "plugins/oas-validator", + "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", From 8991d3453c9c46824e108a7cc669a934bd2fa04f Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Sat, 9 May 2026 17:42:59 +0800 Subject: [PATCH 11/13] docs: fix duplicate oas-validator entry in config.json --- docs/en/latest/config.json | 1 - docs/zh/latest/config.json | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json index 52d018afbcd4..e5459e5bdfc5 100644 --- a/docs/en/latest/config.json +++ b/docs/en/latest/config.json @@ -163,7 +163,6 @@ "plugins/proxy-cache", "plugins/request-validation", "plugins/oas-validator", - "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json index 3da3f8a1cc63..94733997389b 100644 --- a/docs/zh/latest/config.json +++ b/docs/zh/latest/config.json @@ -152,7 +152,6 @@ "plugins/proxy-cache", "plugins/request-validation", "plugins/oas-validator", - "plugins/oas-validator", "plugins/proxy-mirror", "plugins/api-breaker", "plugins/traffic-split", From 1d6ad381912f8b6623d504706a7754be82bc74d9 Mon Sep 17 00:00:00 2001 From: rongxin Date: Mon, 11 May 2026 09:56:24 +0800 Subject: [PATCH 12/13] use conf._mata.validator --- apisix/plugins/oas-validator.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua index 791c0303d321..bbdb2e4fb84f 100644 --- a/apisix/plugins/oas-validator.lua +++ b/apisix/plugins/oas-validator.lua @@ -201,14 +201,14 @@ end local function get_validator(conf) if conf.spec then - if not conf._validator then + if not conf._meta.validator then local validator, err = ov.compile(conf.spec) if not validator then return nil, "failed to compile openapi spec, err: " .. err end - conf._validator = validator + conf._meta.validator = validator end - return conf._validator + return conf._meta.validator end local lrucache = get_spec_url_lrucache() From 2545352a94598cae94abd70273dcfd6c4543ca8e Mon Sep 17 00:00:00 2001 From: rongxin Date: Mon, 11 May 2026 10:46:25 +0800 Subject: [PATCH 13/13] fix --- apisix/plugins/oas-validator.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/apisix/plugins/oas-validator.lua b/apisix/plugins/oas-validator.lua index bbdb2e4fb84f..0cdd5b4a018c 100644 --- a/apisix/plugins/oas-validator.lua +++ b/apisix/plugins/oas-validator.lua @@ -201,6 +201,7 @@ end local function get_validator(conf) if conf.spec then + conf._meta = conf._meta or {} if not conf._meta.validator then local validator, err = ov.compile(conf.spec) if not validator then