This is an automated email from the ASF dual-hosted git repository.

AlexStocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go-pixiu.git


The following commit(s) were added to refs/heads/develop by this push:
     new daaeb6e1f Feature/openapi sdk validator (#948)
daaeb6e1f is described below

commit daaeb6e1f1953403e737155807e50a558034369c
Author: Skylm808 <[email protected]>
AuthorDate: Mon Jun 29 09:37:24 2026 +0800

    Feature/openapi sdk validator (#948)
    
    * Add OpenAPI request validation filter
    
    
    ---------
    
    Co-authored-by: Tianlm <[email protected]>
---
 docs/user/filter/openapi.md                        |  94 ++-
 docs/user/filter/openapi_CN.md                     |  88 ++-
 go.mod                                             |  23 +-
 go.sum                                             |  43 +-
 pkg/common/constant/key.go                         |   1 +
 pkg/context/http/erroresponse.go                   |   3 +
 pkg/filter/http/apiconfig/api_config.go            |  57 +-
 pkg/filter/http/apiconfig/api_config_test.go       | 249 +++---
 pkg/filter/http/apiconfig/config.go                |  37 +
 pkg/filter/http/apiconfig/openapi/compiler.go      | 284 -------
 pkg/filter/http/apiconfig/openapi/compiler_test.go | 203 -----
 pkg/filter/http/apiconfig/openapi/doc.go           |  20 -
 pkg/filter/http/apiconfig/openapi/metadata.go      |  37 -
 pkg/filter/http/apiconfig/openapi/model.go         |  55 --
 pkg/filter/http/apiconfig/openapi/validator.go     | 292 --------
 .../http/apiconfig/openapi/validator_test.go       | 201 -----
 pkg/filter/http/openapi/openapi.go                 | 567 ++++++++++++++
 pkg/filter/http/openapi/openapi_test.go            | 834 +++++++++++++++++++++
 pkg/pluginregistry/registry.go                     |   1 +
 19 files changed, 1711 insertions(+), 1378 deletions(-)

diff --git a/docs/user/filter/openapi.md b/docs/user/filter/openapi.md
index 10c80a343..593050540 100644
--- a/docs/user/filter/openapi.md
+++ b/docs/user/filter/openapi.md
@@ -6,35 +6,34 @@ English | [中文](openapi_CN.md)
 
 ## Overview
 
-Pixiu can load an OpenAPI 3.x file during `dgp.filter.http.apiconfig` 
initialization and merge each operation's
-`ValidationPlan` into the matching route that is already defined by 
`api_config`.
+Pixiu can load a local OpenAPI 3.0/3.1 file in `dgp.filter.http.openapi` and 
validate matching requests before they
+are forwarded upstream. OpenAPI 3.2 documents are supported for the standard 
HTTP operations wired by this filter.
 
-When a request matches an API, Pixiu validates the request before it is 
forwarded upstream.
+Official references:
 
-If validation fails, Pixiu returns a `400 Bad Request` locally and stops the 
filter chain.
+- [libopenapi](https://github.com/pb33f/libopenapi)
+- [libopenapi validation](https://pb33f.io/libopenapi/validation/)
+- [libopenapi validator](https://github.com/pb33f/libopenapi-validator)
 
-## Supported In V1
+If validation fails, Pixiu returns a local `400 Bad Request` and stops the 
filter chain.
 
-- local OpenAPI 3.x file loading
-- path parameter extraction
-- query parameter validation
-- header validation
+## Wired In This Filter
+
+- local OpenAPI 3.0/3.1 file loading, plus OpenAPI 3.2 documents that use 
standard HTTP operations
+- request matching by OpenAPI path and method, including templated paths such 
as `/users/{id}`
+- path, query, and header parameter validation
 - JSON request body validation
-- `required`
-- `type`
-- `enum`
-- `minimum`
-- `maximum`
-- `minLength`
-- `maxLength`
+- OpenAPI schema constraints enforced by the SDK, including `required`, 
`type`, `enum`, `minimum`, `maximum`, `minLength`, and `maxLength`
+
+This filter uses `libopenapi` to parse the spec and `libopenapi-validator` to 
validate incoming requests.
 
-## Not Included In V1
+## Not Wired In This Filter
 
 - response validation
-- remote `$ref`
-- `oneOf` / `allOf` / `anyOf`
+- route creation or `api_config` route matching
+- OpenAPI `security` validation; use dedicated authentication or authorization 
filters for auth checks
+- OpenAPI 3.2 operations outside the standard HTTP request methods wired by 
this filter
 - admin or config-center distribution of OpenAPI files
-- `dynamic + openapi_path` combined configuration
 
 ## Example Filter Config
 
@@ -42,28 +41,57 @@ If validation fails, Pixiu returns a `400 Bad Request` 
locally and stops the fil
 - name: dgp.filter.http.apiconfig
   config:
     path: configs/api_config.yaml
-    openapi_path: configs/openapi_users.yaml
-    enable_openapi_validation: true
+
+- name: dgp.filter.http.openapi
+  config:
+    path: configs/openapi_users.yaml
+    # Optional. Defaults to 1048576 bytes.
+    max_request_body_bytes: 1048576
 ```
 
-OpenAPI validation does not create standalone routes. The route must already 
exist in `api_config`.
+`dgp.filter.http.apiconfig` and `dgp.filter.http.openapi` are independent 
filters. `apiconfig` matches Pixiu API routes
+and writes API metadata into the request context. `openapi` validates only the 
operations declared in the OpenAPI file.
+Deprecated OpenAPI validation keys in `apiconfig`, including `openapi_path` 
and `enable_openapi_validation`, are rejected
+when present, even when set to `""` or `false`. Configure 
`dgp.filter.http.openapi` instead.
+
+If a request path and method are not declared in the OpenAPI file, this filter 
skips validation and lets the request
+continue. If the operation is declared but the request violates parameters or 
body schema, Pixiu returns
+`400 Bad Request`.
 
 ## Notes
 
+- `libopenapi-validator` is an opt-in companion module; `libopenapi` handles 
parsing and model building.
 - Parameter-level validation covers common scalar constraints for `path`, 
`query`, and `header` parameters.
-- V1 does not support using `dynamic` together with `openapi_path` in the same 
filter config.
+- The keywords listed above come from the OpenAPI schema and are enforced by 
the SDK path, not by custom in-repo validators.
+- OpenAPI `security` validation is disabled for this filter, so auth remains 
the responsibility of filters such as JWT,
+  OPA, SAML, or other dedicated authentication and authorization filters.
+- The configured OpenAPI `path` must be relative. Absolute paths, 
parent-directory segments such as `..`, and sensitive
+  base directories are rejected during filter startup. Symlinks are resolved 
before the check so that a relative path
+  cannot bypass the sensitive-directory guard via a symbolic link.
+- Request bodies are capped by `max_request_body_bytes` before schema 
validation. This prevents the validator from
+  reading unbounded JSON or chunked bodies in the gateway hot path.
+- Omit `max_request_body_bytes` or set it to `0` to use the default `1048576` 
byte limit.
+- Relative file references are resolved from the OpenAPI file location, so 
file-based loading keeps local `$ref` paths intact.
+- Invalid OpenAPI documents, including unresolved `$ref` targets, fail during 
filter startup instead of running with a
+  partial validator model.
+- The validator package also exposes response and document validation APIs, 
but this filter only calls the request-validation path.
 
 ## Runtime Flow
 
-1. Pixiu loads `api_config` during `apiconfig.Apply()`.
-2. Pixiu loads the OpenAPI file and compiles validation plans.
-3. Each compiled `ValidationPlan` is merged into the matching 
`router.API.Metadata`.
-4. A request enters `apiconfig.Decode()`.
-5. Pixiu matches the request path and method.
-6. Pixiu extracts the `ValidationPlan` from the matched API metadata.
-7. Pixiu validates the request.
-8. If validation succeeds, the request continues to later proxy filters.
-9. If validation fails, Pixiu responds with `400 Bad Request`.
+1. Pixiu loads the OpenAPI file during `openapi.Apply()` and builds an SDK 
validator.
+2. A request enters `openapi.Decode()`.
+3. Pixiu checks whether the OpenAPI document declares the request path and 
method.
+4. If the operation is not declared, validation is skipped and the request 
continues.
+5. If the operation is declared, Pixiu validates the request through 
`libopenapi-validator`.
+6. If validation succeeds, the request continues to later filters.
+7. If validation fails, Pixiu responds with `400 Bad Request`.
+
+### HEAD requests
+
+The OpenAPI spec does not treat HEAD as a standard HTTP method. When only 
`GET` is declared for a path
+(without an explicit `head` operation), the SDK's `FindPath` treats HEAD as an 
undeclared method.
+The filter therefore skips validation and lets the HEAD request continue to 
downstream handlers.
+If you need HEAD requests to be validated, declare an explicit `head` 
operation in the OpenAPI spec.
 
 ## Example Requests
 
diff --git a/docs/user/filter/openapi_CN.md b/docs/user/filter/openapi_CN.md
index e8a0b2ad9..f3c07c955 100644
--- a/docs/user/filter/openapi_CN.md
+++ b/docs/user/filter/openapi_CN.md
@@ -6,35 +6,34 @@
 
 ## 概述
 
-Pixiu 可以在 `dgp.filter.http.apiconfig` 初始化阶段加载 OpenAPI 3.x 文件,并把每个 operation 的
-`ValidationPlan` 合并到 `api_config` 里已经存在的同名路由上。
+Pixiu 可以在 `dgp.filter.http.openapi` 中加载本地 OpenAPI 3.0/3.1 
文件,并在请求转发到上游之前校验命中的请求。对于
+OpenAPI 3.2 文档,当前仅覆盖这个 filter 已接入的标准 HTTP operation。
 
-当请求命中 API 后,Pixiu 会在转发到上游之前先执行请求校验。
+官方参考:
 
-如果校验失败,Pixiu 会直接返回 `400 Bad Request`,并停止后续过滤链。
+- [libopenapi](https://github.com/pb33f/libopenapi)
+- [libopenapi validation](https://pb33f.io/libopenapi/validation/)
+- [libopenapi validator](https://github.com/pb33f/libopenapi-validator)
 
-## 第一版支持范围
+如果校验失败,Pixiu 会直接返回本地 `400 Bad Request`,并停止后续过滤链。
 
-- 本地 OpenAPI 3.x 文件加载
-- path 参数提取
-- query 参数校验
-- header 校验
+## 当前 filter 已接入
+
+- 本地 OpenAPI 3.0/3.1 文件加载,以及使用标准 HTTP operation 的 OpenAPI 3.2 文档
+- 按 OpenAPI path 和 method 匹配请求,包括 `/users/{id}` 这类模板路径
+- path、query、header 参数校验
 - JSON request body 校验
-- `required`
-- `type`
-- `enum`
-- `minimum`
-- `maximum`
-- `minLength`
-- `maxLength`
+- 由 SDK 执行的 OpenAPI schema 约束,包括 
`required`、`type`、`enum`、`minimum`、`maximum`、`minLength`、`maxLength`
+
+当前这个 filter 使用 `libopenapi` 解析 OpenAPI 文档,并使用 `libopenapi-validator` 
校验进入过滤器的请求。
 
-## 第一版暂不支持
+## 当前 filter 未接入
 
 - response validation
-- 远程 `$ref`
-- `oneOf` / `allOf` / `anyOf`
+- 路由创建或 `api_config` 路由匹配
+- OpenAPI `security` 校验;鉴权请使用专门的认证或授权 filter
+- OpenAPI 3.2 中超出当前标准 HTTP request method 覆盖范围的 operation
 - admin 或配置中心分发 OpenAPI 文件
-- `dynamic + openapi_path` 组合配置
 
 ## 配置示例
 
@@ -42,28 +41,49 @@ Pixiu 可以在 `dgp.filter.http.apiconfig` 初始化阶段加载 OpenAPI 3.x 
 - name: dgp.filter.http.apiconfig
   config:
     path: configs/api_config.yaml
-    openapi_path: configs/openapi_users.yaml
-    enable_openapi_validation: true
+
+- name: dgp.filter.http.openapi
+  config:
+    path: configs/openapi_users.yaml
+    # 可选,默认 1048576 字节。
+    max_request_body_bytes: 1048576
 ```
 
-OpenAPI 校验不会单独创建路由,目标路由必须已经存在于 `api_config` 中。
+`dgp.filter.http.apiconfig` 和 `dgp.filter.http.openapi` 是两个独立 
filter。`apiconfig` 负责匹配 Pixiu API 路由,并把
+API 元信息写入请求上下文;`openapi` 只校验 OpenAPI 文件中声明过的 operation。
+`apiconfig` 中已废弃的 OpenAPI 校验配置键,包括 `openapi_path` 和 
`enable_openapi_validation`,只要出现就会被拒绝;
+即使配置为 `""` 或 `false` 也一样。请改用独立的 `dgp.filter.http.openapi` filter。
+
+如果请求的 path 和 method 没有在 OpenAPI 文件中声明,这个 filter 会跳过校验并放行请求。如果 operation 已声明但请求
+不满足参数或 body schema,Pixiu 会返回 `400 Bad Request`。
 
 ## 说明
 
-- 参数级校验现在覆盖 `path`、`query`、`header` 上常见的标量类型约束。
-- 第一版不支持在同一个 filter 配置里同时使用 `dynamic` 和 `openapi_path`。
+- `libopenapi-validator` 是独立的可选模块,`libopenapi` 负责解析和建模。
+- 参数级校验覆盖 `path`、`query`、`header` 上常见的标量类型约束。
+- 上面这些关键词来自 OpenAPI schema,是由 SDK 路径执行的,不是仓库里再自定义一套验证器。
+- 当前 filter 关闭了 OpenAPI `security` 校验,鉴权仍由 JWT、OPA、SAML 或其他专门的认证/授权 filter 负责。
+- OpenAPI `path` 配置必须使用相对路径。绝对路径、`..` 父目录跳转和敏感 base 目录会在 filter 
启动阶段被拒绝。启动时会解析符号链接(symlink),防止通过相对路径 + 符号链接绕过敏感目录检查。
+- request body 在 schema 校验前会受 `max_request_body_bytes` 限制,避免 validator 
在网关热路径上读取无上限 JSON 或 chunked body。
+- 不配置 `max_request_body_bytes` 或配置为 `0` 时,会使用默认的 `1048576` 字节限制。
+- OpenAPI 文件里的相对引用会按文件所在目录解析,所以使用本地文件加载时可以保留本地 `$ref` 路径。
+- 无效 OpenAPI 文档,包括无法解析的 `$ref`,会在 filter 启动阶段失败,不会继续使用部分构建出来的 validator model。
+- `libopenapi-validator` 也提供响应和文档校验 API,但当前这个 filter 只调用了请求校验入口。
 
 ## 运行流程
 
-1. Pixiu 在 `apiconfig.Apply()` 阶段先加载 `api_config`。
-2. Pixiu 再加载 OpenAPI 文件并编译校验计划。
-3. 每个编译结果的 `ValidationPlan` 会合并到匹配的 `router.API.Metadata` 中。
-4. 请求进入 `apiconfig.Decode()`。
-5. Pixiu 先按 path 和 method 匹配 API。
-6. 从命中的 API metadata 中取出 `ValidationPlan`。
-7. Pixiu 执行请求校验。
-8. 校验通过,请求继续流向后续代理过滤器。
-9. 校验失败,Pixiu 直接返回 `400 Bad Request`。
+1. Pixiu 在 `openapi.Apply()` 阶段加载 OpenAPI 文件并构建 SDK 校验器。
+2. 请求进入 `openapi.Decode()`。
+3. Pixiu 检查 OpenAPI 文档是否声明了请求 path 和 method。
+4. 如果 operation 未声明,跳过校验并继续后续过滤链。
+5. 如果 operation 已声明,Pixiu 通过 `libopenapi-validator` 执行请求校验。
+6. 校验通过,请求继续流向后续 filter。
+7. 校验失败,Pixiu 直接返回 `400 Bad Request`。
+
+### HEAD 请求
+
+OpenAPI spec 不把 HEAD 视为标准 HTTP 方法。当路径只声明了 `GET`(没有显式 `head` operation)时,SDK 的 
`FindPath` 会把 HEAD 当作未声明方法处理,filter 跳过校验并放行。
+如果需要对 HEAD 请求做校验,可以在 OpenAPI spec 中显式声明 `head` operation。
 
 ## 请求示例
 
diff --git a/go.mod b/go.mod
index 3aff8e2ff..b50f0f277 100644
--- a/go.mod
+++ b/go.mod
@@ -38,7 +38,8 @@ require (
        github.com/nacos-group/nacos-sdk-go v1.1.3
        github.com/nacos-group/nacos-sdk-go/v2 v2.3.2
        github.com/open-policy-agent/opa v0.45.0
-       github.com/pb33f/libopenapi v0.28.2
+       github.com/pb33f/libopenapi v0.36.1
+       github.com/pb33f/libopenapi-validator v0.13.7
        github.com/pkg/errors v0.9.1
        github.com/prometheus/client_golang v1.23.0
        github.com/prometheus/common v0.65.0
@@ -62,7 +63,6 @@ require (
        go.opentelemetry.io/otel/sdk/metric v1.43.0
        go.opentelemetry.io/otel/trace v1.43.0
        go.uber.org/zap v1.21.0
-       go.yaml.in/yaml/v4 v4.0.0-rc.2
        golang.org/x/crypto v0.49.0
        golang.org/x/net v0.52.0
        golang.org/x/sync v0.20.0
@@ -74,6 +74,10 @@ require (
        v.marlon.life/toolkit v0.0.0-20211025131614-e4a91730b4ab
 )
 
+// Pin v2.0.2 to match github.com/go-openapi/spec v0.22.1 test dependencies
+// pulled through swaggo/swag; newer testify/v2 versions are not compatible 
with that test tree.
+replace github.com/go-openapi/testify/v2 => github.com/go-openapi/testify/v2 
v2.0.2
+
 require (
        cel.dev/expr v0.15.0 // indirect
        filippo.io/edwards25519 v1.1.1 // indirect
@@ -105,11 +109,12 @@ require (
        github.com/aliyun/credentials-go v1.4.3 // indirect
        github.com/apolloconfig/agollo/v4 v4.4.0 // indirect
        github.com/bahlo/generic-list-go v0.2.0 // indirect
+       github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad // 
indirect
        github.com/beevik/etree v1.5.0 // indirect
        github.com/beorn7/perks v1.0.1 // indirect
        github.com/bits-and-blooms/bitset v1.2.0 // indirect
        github.com/bufbuild/protocompile v0.14.1 // indirect
-       github.com/buger/jsonparser v1.1.1 // indirect
+       github.com/buger/jsonparser v1.1.2 // indirect
        github.com/bytedance/sonic v1.11.6 // indirect
        github.com/bytedance/sonic/loader v0.1.1 // indirect
        github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@@ -123,7 +128,7 @@ require (
        github.com/davecgh/go-spew v1.1.1 // indirect
        github.com/deckarep/golang-set v1.7.1 // indirect
        github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
-       github.com/dlclark/regexp2 v1.7.0 // indirect
+       github.com/dlclark/regexp2 v1.12.0 // indirect
        github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 // indirect
        github.com/dustin/go-humanize v1.0.1 // indirect
        github.com/eapache/go-resiliency v1.7.0 // indirect
@@ -137,11 +142,11 @@ require (
        github.com/go-logr/logr v1.4.3 // indirect
        github.com/go-logr/stdr v1.2.2 // indirect
        github.com/go-ole/go-ole v1.2.6 // indirect
-       github.com/go-openapi/jsonpointer v0.22.3 // indirect
+       github.com/go-openapi/jsonpointer v0.23.1 // indirect
        github.com/go-openapi/jsonreference v0.21.3 // indirect
        github.com/go-openapi/spec v0.22.1 // indirect
        github.com/go-openapi/swag/conv v0.25.4 // indirect
-       github.com/go-openapi/swag/jsonname v0.25.4 // indirect
+       github.com/go-openapi/swag/jsonname v0.26.0 // indirect
        github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
        github.com/go-openapi/swag/loading v0.25.4 // indirect
        github.com/go-openapi/swag/stringutils v0.25.4 // indirect
@@ -208,8 +213,8 @@ require (
        github.com/opentracing/opentracing-go v1.2.0 // indirect
        github.com/openzipkin/zipkin-go v0.4.2 // indirect
        github.com/orcaman/concurrent-map v0.0.0-20210501183033-44dafcb38ecc // 
indirect
-       github.com/pb33f/jsonpath v0.1.2 // indirect
-       github.com/pb33f/ordered-map/v2 v2.3.0 // indirect
+       github.com/pb33f/jsonpath v0.8.2 // indirect
+       github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
        github.com/pelletier/go-toml v1.9.3 // indirect
        github.com/pelletier/go-toml/v2 v2.2.2 // indirect
        github.com/pierrec/lz4 v2.6.1+incompatible // indirect
@@ -224,6 +229,7 @@ require (
        github.com/quic-go/quic-go v0.59.1 // indirect
        github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // 
indirect
        github.com/russellhaering/goxmldsig v1.4.0 // indirect
+       github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
        github.com/segmentio/asm v1.2.1 // indirect
        github.com/shirou/gopsutil/v3 v3.22.2 // indirect
        github.com/sirupsen/logrus v1.9.4 // indirect
@@ -258,6 +264,7 @@ require (
        go.uber.org/atomic v1.10.0 // indirect
        go.uber.org/multierr v1.8.0 // indirect
        go.yaml.in/yaml/v3 v3.0.4 // indirect
+       go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
        golang.org/x/arch v0.20.0 // indirect
        golang.org/x/mod v0.34.0 // indirect
        golang.org/x/sys v0.42.0 // indirect
diff --git a/go.sum b/go.sum
index cee292724..5f5ccdb23 100644
--- a/go.sum
+++ b/go.sum
@@ -182,6 +182,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod 
h1:8EzeIqfWt2wWT4rJVu3f21
 github.com/aws/smithy-go v1.8.0/go.mod 
h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 github.com/bahlo/generic-list-go v0.2.0 
h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
 github.com/bahlo/generic-list-go v0.2.0/go.mod 
h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad 
h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8=
+github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod 
h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0=
 github.com/beevik/etree v1.1.0/go.mod 
h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
 github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
 github.com/beevik/etree v1.5.0/go.mod 
h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
@@ -192,6 +194,8 @@ github.com/beorn7/perks v1.0.0/go.mod 
h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod 
h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bgentry/speakeasy v0.1.0/go.mod 
h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bitly/go-simplejson v0.5.1 
h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
+github.com/bitly/go-simplejson v0.5.1/go.mod 
h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
 github.com/bits-and-blooms/bitset v1.2.0 
h1:Kn4yilvwNtMACtf1eYDlG8H77R07mZSPbMjLyS07ChA=
 github.com/bits-and-blooms/bitset v1.2.0/go.mod 
h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod 
h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
@@ -199,8 +203,9 @@ github.com/bketelsen/crypt v0.0.4/go.mod 
h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
 github.com/bufbuild/protocompile v0.14.1 
h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
 github.com/bufbuild/protocompile v0.14.1/go.mod 
h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
 github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod 
h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
-github.com/buger/jsonparser v1.1.1 
h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod 
h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/buger/jsonparser v1.1.2 
h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
+github.com/buger/jsonparser v1.1.2/go.mod 
h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/bytecodealliance/wasmtime-go v1.0.0 
h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU=
 github.com/bytecodealliance/wasmtime-go v1.0.0/go.mod 
h1:jjlqQbWUfVSbehpErw3UoWFndBXRRMvfikYH6KsCwOg=
 github.com/bytedance/sonic v1.11.6 
h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
@@ -288,8 +293,9 @@ github.com/dgryski/go-sip13 
v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
 github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 
h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
 github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod 
h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
 github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod 
h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dlclark/regexp2 v1.7.0 
h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
 github.com/dlclark/regexp2 v1.7.0/go.mod 
h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dlclark/regexp2 v1.12.0 
h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
+github.com/dlclark/regexp2 v1.12.0/go.mod 
h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod 
h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
 github.com/dop251/goja v0.0.0-20240220182346-e401ed450204 
h1:O7I1iuzEA7SG+dK8ocOBSlYAA9jBUmCYl/Qa7ey7JAM=
 github.com/dop251/goja v0.0.0-20240220182346-e401ed450204/go.mod 
h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
@@ -394,8 +400,8 @@ github.com/go-logr/stdr v1.2.2/go.mod 
h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
 github.com/go-ole/go-ole v1.2.4/go.mod 
h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 github.com/go-ole/go-ole v1.2.6/go.mod 
h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-openapi/jsonpointer v0.22.3 
h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
-github.com/go-openapi/jsonpointer v0.22.3/go.mod 
h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
+github.com/go-openapi/jsonpointer v0.23.1 
h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
+github.com/go-openapi/jsonpointer v0.23.1/go.mod 
h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
 github.com/go-openapi/jsonreference v0.21.3 
h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
 github.com/go-openapi/jsonreference v0.21.3/go.mod 
h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
 github.com/go-openapi/spec v0.22.1 
h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
@@ -403,8 +409,8 @@ github.com/go-openapi/spec v0.22.1/go.mod 
h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONSt
 github.com/go-openapi/swag v0.19.15 
h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
 github.com/go-openapi/swag/conv v0.25.4 
h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
 github.com/go-openapi/swag/conv v0.25.4/go.mod 
h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
-github.com/go-openapi/swag/jsonname v0.25.4 
h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
-github.com/go-openapi/swag/jsonname v0.25.4/go.mod 
h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
+github.com/go-openapi/swag/jsonname v0.26.0 
h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
+github.com/go-openapi/swag/jsonname v0.26.0/go.mod 
h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
 github.com/go-openapi/swag/jsonutils v0.25.4 
h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
 github.com/go-openapi/swag/jsonutils v0.25.4/go.mod 
h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
 github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 
h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
@@ -869,12 +875,14 @@ github.com/orcaman/concurrent-map 
v0.0.0-20210501183033-44dafcb38ecc/go.mod h1:L
 github.com/pact-foundation/pact-go v1.0.4/go.mod 
h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod 
h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0/go.mod 
h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pb33f/jsonpath v0.1.2 
h1:PlqXjEyecMqoYJupLxYeClCGWEpAFnh4pmzgspbXDPI=
-github.com/pb33f/jsonpath v0.1.2/go.mod 
h1:TtKnUnfqZm48q7a56DxB3WtL3ipkVtukMKGKxaR/uXU=
-github.com/pb33f/libopenapi v0.28.2 
h1:AXVCE8DWzytXu0jv0Z+cXVopnO/bXU1oWvgA9qiRWgw=
-github.com/pb33f/libopenapi v0.28.2/go.mod 
h1:mHMHA3ZKSZDTInNAuUtqkHlKLIjPm2HN1vgsGR57afc=
-github.com/pb33f/ordered-map/v2 v2.3.0 
h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ=
-github.com/pb33f/ordered-map/v2 v2.3.0/go.mod 
h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw=
+github.com/pb33f/jsonpath v0.8.2 
h1:Ou4C7zjYClBm97dfZjDCjdZGusJoynv/vrtiEKNfj2Y=
+github.com/pb33f/jsonpath v0.8.2/go.mod 
h1:zBV5LJW4OQOPatmQE2QdKpGQJvhDTlE5IEj6ASaRNTo=
+github.com/pb33f/libopenapi v0.36.1 
h1:CNZ52e+/W9fA1kAgL8EePDQQrKPfN9+HdLR6XAxUEpw=
+github.com/pb33f/libopenapi v0.36.1/go.mod 
h1:MsDdUlQ1CdrIDO5v26JfgBxQs7kcaOUEpMP3EqU6bI4=
+github.com/pb33f/libopenapi-validator v0.13.7 
h1:9rMZjvoG9Qp9u2pr//EKl0cM+Qiwve5pY3Os0qoRC0M=
+github.com/pb33f/libopenapi-validator v0.13.7/go.mod 
h1:zkbQL07GQANm5UNSvm7Lk5ofDL9GnD4TyCnzHus5lZI=
+github.com/pb33f/ordered-map/v2 v2.3.1 
h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
+github.com/pb33f/ordered-map/v2 v2.3.1/go.mod 
h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
 github.com/pborman/uuid v1.2.0/go.mod 
h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
 github.com/pelletier/go-toml v1.2.0/go.mod 
h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.7.0/go.mod 
h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
@@ -976,6 +984,8 @@ github.com/ryanuber/columnize 
v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod 
h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/go-glob v1.0.0/go.mod 
h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod 
h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 
h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod 
h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod 
h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
 github.com/segmentio/asm v1.2.1/go.mod 
h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
@@ -1213,8 +1223,8 @@ go.uber.org/zap v1.21.0 
h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
 go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod 
h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
-go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
-go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod 
h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
+go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
+go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod 
h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod 
h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
 golang.org/x/arch v0.20.0/go.mod 
h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
@@ -1237,6 +1247,7 @@ golang.org/x/crypto 
v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.6.0/go.mod 
h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 golang.org/x/crypto v0.10.0/go.mod 
h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0/go.mod 
h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
 golang.org/x/crypto v0.14.0/go.mod 
h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
 golang.org/x/crypto v0.18.0/go.mod 
h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.19.0/go.mod 
h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
@@ -1347,6 +1358,7 @@ golang.org/x/net v0.6.0/go.mod 
h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
@@ -1475,6 +1487,7 @@ golang.org/x/sys v0.5.0/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -1486,6 +1499,7 @@ golang.org/x/term 
v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.10.0/go.mod 
h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 golang.org/x/term v0.13.0/go.mod 
h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/term v0.16.0/go.mod 
h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/term v0.17.0/go.mod 
h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
@@ -1504,6 +1518,7 @@ golang.org/x/text v0.3.8/go.mod 
h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.10.0/go.mod 
h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0/go.mod 
h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod 
h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod 
h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
diff --git a/pkg/common/constant/key.go b/pkg/common/constant/key.go
index b69cc5f57..721361481 100644
--- a/pkg/common/constant/key.go
+++ b/pkg/common/constant/key.go
@@ -37,6 +37,7 @@ const (
        HTTPDubboProxyFilter       = "dgp.filter.http.dubboproxy"
        HTTPDirectDubboProxyFilter = "dgp.filter.http.directdubboproxy"
        HTTPApiConfigFilter        = "dgp.filter.http.apiconfig"
+       HTTPOpenAPIFilter          = "dgp.filter.http.openapi"
        HTTPTimeoutFilter          = "dgp.filter.http.timeout"
        TracingFilter              = "dgp.filters.tracing"
        HTTPWasmFilter             = "dgp.filter.http.webassembly"
diff --git a/pkg/context/http/erroresponse.go b/pkg/context/http/erroresponse.go
index 97587b625..70aeb3b02 100644
--- a/pkg/context/http/erroresponse.go
+++ b/pkg/context/http/erroresponse.go
@@ -61,6 +61,9 @@ var (
        // 429 - Rate Limited
        RateLimited = newErrorBuilder(http.StatusTooManyRequests, "Rate 
limited")
 
+       // 413 - Payload Too Large
+       PayloadTooLarge = newErrorBuilder(http.StatusRequestEntityTooLarge, 
"Payload too large")
+
        // 500 - Internal Server Error
        InternalError      = newErrorBuilder(http.StatusInternalServerError, 
"Internal server error")
        ConfigurationError = newErrorBuilder(http.StatusInternalServerError, 
"Configuration error")
diff --git a/pkg/filter/http/apiconfig/api_config.go 
b/pkg/filter/http/apiconfig/api_config.go
index e48d72ba5..7918e673a 100644
--- a/pkg/filter/http/apiconfig/api_config.go
+++ b/pkg/filter/http/apiconfig/api_config.go
@@ -17,10 +17,6 @@
 
 package apiconfig
 
-import (
-       "os"
-)
-
 import (
        "github.com/pkg/errors"
 )
@@ -31,7 +27,6 @@ import (
        "github.com/apache/dubbo-go-pixiu/pkg/config"
        contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
        "github.com/apache/dubbo-go-pixiu/pkg/filter/http/apiconfig/api"
-       "github.com/apache/dubbo-go-pixiu/pkg/filter/http/apiconfig/openapi"
        "github.com/apache/dubbo-go-pixiu/pkg/logger"
        "github.com/apache/dubbo-go-pixiu/pkg/router"
        "github.com/apache/dubbo-go-pixiu/pkg/server"
@@ -74,8 +69,8 @@ func (factory *FilterFactory) Config() any {
 func (factory *FilterFactory) Apply() error {
        factory.apiService = api.NewLocalMemoryAPIDiscoveryService()
 
-       if factory.cfg.Dynamic && (factory.cfg.EnableOpenAPIValidation || 
factory.cfg.OpenAPIPath != "") {
-               return errors.New("dynamic api config does not support openapi 
validation")
+       if factory.cfg.hasDeprecatedOpenAPIConfig() {
+               return errors.New("openapi_path and enable_openapi_validation 
have moved out of apiconfig; configure dgp.filter.http.openapi instead")
        }
 
        if factory.cfg.Dynamic {
@@ -84,9 +79,6 @@ func (factory *FilterFactory) Apply() error {
        }
 
        if factory.cfg.Path == "" && factory.cfg.APIMetaConfig == nil {
-               if factory.cfg.EnableOpenAPIValidation && 
factory.cfg.OpenAPIPath != "" {
-                       logger.Warn("openapi validation is configured without 
api config; skip openapi validation")
-               }
                return nil
        }
 
@@ -98,12 +90,6 @@ func (factory *FilterFactory) Apply() error {
                return err
        }
 
-       if factory.cfg.EnableOpenAPIValidation && factory.cfg.OpenAPIPath != "" 
{
-               if err := 
factory.mergeOpenAPIFromFile(factory.cfg.OpenAPIPath); err != nil {
-                       return err
-               }
-       }
-
        return nil
 }
 
@@ -147,14 +133,6 @@ func (f *Filter) Decode(ctx *contexthttp.HttpContext) 
filter.FilterStatus {
                return filter.Stop
        }
 
-       if plan := openapi.ExtractValidationPlan(v); plan != nil {
-               if err := openapi.ValidateRequest(req, plan); err != nil {
-                       errResp := contexthttp.BadRequest.WithError(err)
-                       ctx.SendLocalReply(errResp.Status, errResp.ToJSON())
-                       logger.Debug(errResp.Error())
-                       return filter.Stop
-               }
-       }
        ctx.API(v)
        return filter.Continue
 }
@@ -163,37 +141,6 @@ func (factory *FilterFactory) GetApiService() 
api.APIDiscoveryService {
        return factory.apiService
 }
 
-func (factory *FilterFactory) mergeOpenAPIFromFile(path string) error {
-       spec, err := os.ReadFile(path)
-       if err != nil {
-               return err
-       }
-
-       doc, err := openapi.LoadDocumentFromBytes(spec)
-       if err != nil {
-               return err
-       }
-
-       compiledRoutes, err := openapi.CompileRoutes(doc)
-       if err != nil {
-               return err
-       }
-       for _, compiled := range compiledRoutes {
-               if _, err := 
factory.apiService.GetAPI(compiled.Route.URLPattern, compiled.Route.HTTPVerb); 
err != nil {
-                       logger.Warnf(
-                               "skip openapi validation for %s %s because api 
config route does not exist",
-                               compiled.Route.HTTPVerb,
-                               compiled.Route.URLPattern,
-                       )
-                       continue
-               }
-               if err := factory.apiService.MergeAPI(compiled.Route); err != 
nil {
-                       return err
-               }
-       }
-       return nil
-}
-
 // initApiConfig return value of the bool is for the judgment of whether is a 
api meta data error, a kind of silly (?)
 func initApiConfig(cf *ApiConfigConfig) (*config.APIConfig, error) {
        if cf.APIMetaConfig != nil {
diff --git a/pkg/filter/http/apiconfig/api_config_test.go 
b/pkg/filter/http/apiconfig/api_config_test.go
index ff6b46926..1942a130d 100644
--- a/pkg/filter/http/apiconfig/api_config_test.go
+++ b/pkg/filter/http/apiconfig/api_config_test.go
@@ -21,169 +21,97 @@ import (
        "net/http"
        "net/http/httptest"
        "os"
+       "path/filepath"
        "testing"
 )
 
 import (
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+
+       "gopkg.in/yaml.v3"
 )
 
 import (
-       "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
        extfilter "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
-       "github.com/apache/dubbo-go-pixiu/pkg/config"
        contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
-       "github.com/apache/dubbo-go-pixiu/pkg/filter/http/apiconfig/api"
-       "github.com/apache/dubbo-go-pixiu/pkg/filter/http/apiconfig/openapi"
-       "github.com/apache/dubbo-go-pixiu/pkg/router"
 )
 
-func TestDecode_StopsOnOpenAPIValidationFailure(t *testing.T) {
-       apiService := api.NewLocalMemoryAPIDiscoveryService()
-       err := apiService.AddAPI(router.API{
-               URLPattern: "/users",
-               Method: config.Method{
-                       Enable:   true,
-                       HTTPVerb: constant.Get,
-               },
-               Metadata: map[string]any{
-                       openapi.ValidationPlanMetadataKey: 
&openapi.ValidationPlan{
-                               QueryParameters: []openapi.ParameterValidation{
-                                       {Name: "id", Required: true},
-                               },
-                       },
-               },
-       })
-       require.NoError(t, err)
+func TestDecode_ContinuesWhenRouteMatches(t *testing.T) {
+       factory := newAPIConfigFactory(t, `
+name: api name
+resources:
+  - path: /users
+    type: restful
+    methods:
+      - httpVerb: GET
+        enable: true
+`)
 
-       filterInstance := &Filter{apiService: apiService}
+       filterInstance := &Filter{apiService: factory.apiService}
        req := httptest.NewRequest(http.MethodGet, "/users", nil)
        recorder := httptest.NewRecorder()
        ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
 
        status := filterInstance.Decode(ctx)
 
-       assert.Equal(t, extfilter.Stop, status)
-       assert.Equal(t, http.StatusBadRequest, recorder.Code)
-       assert.True(t, ctx.LocalReply())
-       assert.Nil(t, ctx.GetAPI())
+       assert.Equal(t, extfilter.Continue, status)
+       require.NotNil(t, ctx.GetAPI())
+       assert.Equal(t, "/users", ctx.GetAPI().URLPattern)
+       assert.Equal(t, http.MethodGet, ctx.GetAPI().HTTPVerb)
+       assert.False(t, ctx.LocalReply())
 }
 
-func TestDecode_ContinuesOnOpenAPIValidationSuccess(t *testing.T) {
-       apiService := api.NewLocalMemoryAPIDiscoveryService()
-       err := apiService.AddAPI(router.API{
-               URLPattern: "/users",
-               Method: config.Method{
-                       Enable:   true,
-                       HTTPVerb: constant.Get,
-               },
-               Metadata: map[string]any{
-                       openapi.ValidationPlanMetadataKey: 
&openapi.ValidationPlan{
-                               QueryParameters: []openapi.ParameterValidation{
-                                       {Name: "id", Required: true},
-                               },
-                       },
-               },
-       })
-       require.NoError(t, err)
+func TestDecode_StopsWhenRouteIsMissing(t *testing.T) {
+       factory := newAPIConfigFactory(t, `
+name: api name
+resources:
+  - path: /users
+    type: restful
+    methods:
+      - httpVerb: GET
+        enable: true
+`)
 
-       filterInstance := &Filter{apiService: apiService}
-       req := httptest.NewRequest(http.MethodGet, "/users?id=123", nil)
+       filterInstance := &Filter{apiService: factory.apiService}
+       req := httptest.NewRequest(http.MethodGet, "/orders", nil)
        recorder := httptest.NewRecorder()
        ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
 
        status := filterInstance.Decode(ctx)
 
-       assert.Equal(t, extfilter.Continue, status)
-       require.NotNil(t, ctx.GetAPI())
-       assert.Equal(t, "/users", ctx.GetAPI().URLPattern)
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusNotFound, recorder.Code)
+       assert.True(t, ctx.LocalReply())
+       assert.Nil(t, ctx.GetAPI())
 }
 
-func TestDecode_StopsOnOpenAPIParameterTypeFailure(t *testing.T) {
-       apiService := api.NewLocalMemoryAPIDiscoveryService()
-       err := apiService.AddAPI(router.API{
-               URLPattern: "/users",
-               Method: config.Method{
-                       Enable:   true,
-                       HTTPVerb: constant.Get,
-               },
-               Metadata: map[string]any{
-                       openapi.ValidationPlanMetadataKey: 
&openapi.ValidationPlan{
-                               QueryParameters: []openapi.ParameterValidation{
-                                       {Name: "page", Type: "integer", 
Required: true},
-                               },
-                       },
-               },
-       })
-       require.NoError(t, err)
+func TestDecode_StopsWhenRouteIsDisabled(t *testing.T) {
+       factory := newAPIConfigFactory(t, `
+name: api name
+resources:
+  - path: /users
+    type: restful
+    methods:
+      - httpVerb: GET
+        enable: false
+`)
 
-       filterInstance := &Filter{apiService: apiService}
-       req := httptest.NewRequest(http.MethodGet, "/users?page=abc", nil)
+       filterInstance := &Filter{apiService: factory.apiService}
+       req := httptest.NewRequest(http.MethodGet, "/users", nil)
        recorder := httptest.NewRecorder()
        ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
 
        status := filterInstance.Decode(ctx)
 
        assert.Equal(t, extfilter.Stop, status)
-       assert.Equal(t, http.StatusBadRequest, recorder.Code)
+       assert.Equal(t, http.StatusNotAcceptable, recorder.Code)
        assert.True(t, ctx.LocalReply())
        assert.Nil(t, ctx.GetAPI())
 }
 
-func TestApply_MergesOpenAPIRoutesFromFile(t *testing.T) {
-       specFile, err := os.CreateTemp(t.TempDir(), "openapi-*.yaml")
-       require.NoError(t, err)
-       defer specFile.Close()
-
-       _, err = specFile.WriteString(`
-openapi: 3.0.3
-info:
-  title: users
-  version: "1.0.0"
-paths:
-  /users/{id}:
-    get:
-      parameters:
-        - name: id
-          in: path
-          required: true
-          schema:
-            type: string
-      responses:
-        "200":
-          description: ok
-  /users:
-    post:
-      parameters:
-        - name: source
-          in: query
-          required: true
-          schema:
-            type: string
-            enum: [web, app]
-      requestBody:
-        required: true
-        content:
-          application/json:
-            schema:
-              type: object
-              required: [name]
-              properties:
-                name:
-                  type: string
-                  maxLength: 32
-      responses:
-        "200":
-          description: ok
-`)
-       require.NoError(t, err)
-
-       apiConfigFile, err := os.CreateTemp(t.TempDir(), "api-config-*.yaml")
-       require.NoError(t, err)
-       defer apiConfigFile.Close()
-
-       _, err = apiConfigFile.WriteString(`
+func TestApply_KeepsNestedRouteMatching(t *testing.T) {
+       factory := newAPIConfigFactory(t, `
 name: api name
 resources:
   - path: /users
@@ -198,43 +126,80 @@ resources:
           - httpVerb: GET
             enable: true
 `)
-       require.NoError(t, err)
-
-       factory := &FilterFactory{
-               cfg: &ApiConfigConfig{
-                       Path:                    apiConfigFile.Name(),
-                       OpenAPIPath:             specFile.Name(),
-                       EnableOpenAPIValidation: true,
-               },
-       }
-
-       err = factory.Apply()
-       require.NoError(t, err)
 
        matched, err := factory.apiService.MatchAPI("/users", http.MethodPost)
        require.NoError(t, err)
-       require.NotNil(t, openapi.ExtractValidationPlan(matched))
+       require.NotNil(t, matched)
        assert.Equal(t, "/users", matched.URLPattern)
        assert.Equal(t, http.MethodPost, matched.HTTPVerb)
 
        pathMatched, err := factory.apiService.MatchAPI("/users/42", 
http.MethodGet)
        require.NoError(t, err)
-       require.NotNil(t, openapi.ExtractValidationPlan(pathMatched))
+       require.NotNil(t, pathMatched)
        assert.Equal(t, "/users/:id", pathMatched.URLPattern)
 }
 
-func TestApply_RejectsDynamicOpenAPIValidationCombination(t *testing.T) {
+func TestApply_RejectsDeprecatedOpenAPIConfig(t *testing.T) {
        factory := &FilterFactory{
                cfg: &ApiConfigConfig{
-                       Dynamic:                 true,
-                       DynamicAdapter:          "mock",
-                       OpenAPIPath:             "configs/openapi_users.yaml",
+                       OpenAPIPath:             "configs/openapi.yaml",
                        EnableOpenAPIValidation: true,
                },
        }
 
        err := factory.Apply()
+
        require.Error(t, err)
-       assert.ErrorContains(t, err, "dynamic")
-       assert.ErrorContains(t, err, "openapi")
+       assert.Contains(t, err.Error(), "dgp.filter.http.openapi")
+}
+
+func TestApply_RejectsDeprecatedOpenAPIConfigPresence(t *testing.T) {
+       tests := []struct {
+               name string
+               yaml string
+       }{
+               {
+                       name: "empty openapi path",
+                       yaml: `
+path: configs/api_config.yaml
+openapi_path: ""
+`,
+               },
+               {
+                       name: "disabled openapi validation",
+                       yaml: `
+path: configs/api_config.yaml
+enable_openapi_validation: false
+`,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       cfg := &ApiConfigConfig{}
+                       require.NoError(t, yaml.Unmarshal([]byte(tt.yaml), cfg))
+                       factory := &FilterFactory{cfg: cfg}
+
+                       err := factory.Apply()
+
+                       require.Error(t, err)
+                       assert.Contains(t, err.Error(), 
"dgp.filter.http.openapi")
+               })
+       }
+}
+
+func newAPIConfigFactory(t *testing.T, apiConfig string) *FilterFactory {
+       t.Helper()
+
+       dir := t.TempDir()
+       apiConfigPath := filepath.Join(dir, "api-config.yaml")
+       require.NoError(t, os.WriteFile(apiConfigPath, []byte(apiConfig), 
0o600))
+
+       factory := &FilterFactory{
+               cfg: &ApiConfigConfig{
+                       Path: apiConfigPath,
+               },
+       }
+       require.NoError(t, factory.Apply())
+       return factory
 }
diff --git a/pkg/filter/http/apiconfig/config.go 
b/pkg/filter/http/apiconfig/config.go
index f7f2513e7..d5178a521 100644
--- a/pkg/filter/http/apiconfig/config.go
+++ b/pkg/filter/http/apiconfig/config.go
@@ -17,6 +17,10 @@
 
 package apiconfig
 
+import (
+       "gopkg.in/yaml.v3"
+)
+
 import (
        "github.com/apache/dubbo-go-pixiu/pkg/model"
 )
@@ -29,4 +33,37 @@ type ApiConfigConfig struct {
        DynamicAdapter          string               `yaml:"dynamic_adapter" 
json:"dynamic_adapter,omitempty"`
        OpenAPIPath             string               `yaml:"openapi_path" 
json:"openapi_path,omitempty"`
        EnableOpenAPIValidation bool                 
`yaml:"enable_openapi_validation" json:"enable_openapi_validation,omitempty"`
+
+       deprecatedOpenAPIPathSet             bool
+       deprecatedEnableOpenAPIValidationSet bool
+}
+
+func (c *ApiConfigConfig) UnmarshalYAML(value *yaml.Node) error {
+       type rawConfig ApiConfigConfig
+       var decoded rawConfig
+       if err := value.Decode(&decoded); err != nil {
+               return err
+       }
+
+       *c = ApiConfigConfig(decoded)
+       if value.Kind != yaml.MappingNode {
+               return nil
+       }
+
+       for i := 0; i+1 < len(value.Content); i += 2 {
+               switch value.Content[i].Value {
+               case "openapi_path":
+                       c.deprecatedOpenAPIPathSet = true
+               case "enable_openapi_validation":
+                       c.deprecatedEnableOpenAPIValidationSet = true
+               }
+       }
+       return nil
+}
+
+func (c *ApiConfigConfig) hasDeprecatedOpenAPIConfig() bool {
+       return c.OpenAPIPath != "" ||
+               c.EnableOpenAPIValidation ||
+               c.deprecatedOpenAPIPathSet ||
+               c.deprecatedEnableOpenAPIValidationSet
 }
diff --git a/pkg/filter/http/apiconfig/openapi/compiler.go 
b/pkg/filter/http/apiconfig/openapi/compiler.go
deleted file mode 100644
index 646f8a065..000000000
--- a/pkg/filter/http/apiconfig/openapi/compiler.go
+++ /dev/null
@@ -1,284 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-import (
-       "fmt"
-       "regexp"
-       "strings"
-)
-
-import (
-       "github.com/pb33f/libopenapi"
-       "github.com/pb33f/libopenapi/datamodel/high/base"
-       v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
-
-       "go.yaml.in/yaml/v4"
-)
-
-import (
-       "github.com/apache/dubbo-go-pixiu/pkg/config"
-       "github.com/apache/dubbo-go-pixiu/pkg/router"
-)
-
-var openAPIPathParamPattern = regexp.MustCompile(`\{([^}/]+)\}`)
-
-type CompiledRoute struct {
-       Route      router.API
-       Validation *ValidationPlan
-}
-
-func LoadDocumentFromBytes(spec []byte) (libopenapi.Document, error) {
-       return libopenapi.NewDocument(spec)
-}
-
-func CompileRoutes(doc libopenapi.Document) ([]CompiledRoute, error) {
-       model, err := doc.BuildV3Model()
-       if err != nil {
-               return nil, err
-       }
-       if model == nil || model.Model.Paths == nil {
-               return nil, fmt.Errorf("openapi document does not contain 
paths")
-       }
-
-       var compiled []CompiledRoute
-       for path, item := range model.Model.Paths.PathItems.FromOldest() {
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "GET"}, item, item.Get)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "PUT"}, item, item.Put)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "POST"}, item, item.Post)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "DELETE"}, item, item.Delete)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "PATCH"}, item, item.Patch)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "HEAD"}, item, item.Head)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "OPTIONS"}, item, item.Options)
-               compiled = appendCompiledRoute(compiled, path, 
config.Method{Enable: true, HTTPVerb: "TRACE"}, item, item.Trace)
-       }
-
-       return compiled, nil
-}
-
-func appendCompiledRoute(compiled []CompiledRoute, path string, method 
config.Method, pathItem *v3.PathItem, operation *v3.Operation) []CompiledRoute {
-       if operation == nil {
-               return compiled
-       }
-
-       validationPlan := compileValidationPlan(path, pathItem, operation)
-
-       return append(compiled, CompiledRoute{
-               Route: router.API{
-                       URLPattern: normalizeOpenAPIPath(path),
-                       Method:     method,
-                       Metadata: map[string]any{
-                               ValidationPlanMetadataKey: validationPlan,
-                       },
-               },
-               Validation: validationPlan,
-       })
-}
-
-func compileValidationPlan(path string, pathItem *v3.PathItem, operation 
*v3.Operation) *ValidationPlan {
-       plan := &ValidationPlan{}
-       if operation == nil {
-               return plan
-       }
-       plan.RoutePattern = normalizeOpenAPIPath(path)
-
-       for _, parameter := range mergedParameters(pathItem, operation) {
-               if parameter == nil {
-                       continue
-               }
-               compiled := compileParameter(parameter)
-               switch strings.ToLower(parameter.In) {
-               case "path":
-                       plan.PathParameters = append(plan.PathParameters, 
compiled)
-               case "query":
-                       plan.QueryParameters = append(plan.QueryParameters, 
compiled)
-               case "header":
-                       plan.HeaderParameters = append(plan.HeaderParameters, 
compiled)
-               }
-       }
-
-       plan.RequestBody = compileRequestBody(operation.RequestBody)
-       return plan
-}
-
-func mergedParameters(pathItem *v3.PathItem, operation *v3.Operation) 
[]*v3.Parameter {
-       var parameters []*v3.Parameter
-       if pathItem != nil {
-               parameters = append(parameters, pathItem.Parameters...)
-       }
-       if operation == nil {
-               return parameters
-       }
-
-       for _, parameter := range operation.Parameters {
-               if parameter == nil {
-                       parameters = append(parameters, nil)
-                       continue
-               }
-               replaced := false
-               for idx, existing := range parameters {
-                       if existing == nil {
-                               continue
-                       }
-                       if strings.EqualFold(existing.Name, parameter.Name) && 
strings.EqualFold(existing.In, parameter.In) {
-                               parameters[idx] = parameter
-                               replaced = true
-                               break
-                       }
-               }
-               if !replaced {
-                       parameters = append(parameters, parameter)
-               }
-       }
-
-       return parameters
-}
-
-func compileParameter(parameter *v3.Parameter) ParameterValidation {
-       compiled := ParameterValidation{
-               Name: parameter.Name,
-       }
-       if parameter.Required != nil {
-               compiled.Required = *parameter.Required
-       }
-       if parameter.Schema != nil {
-               schema, err := parameter.Schema.BuildSchema()
-               if err == nil && schema != nil {
-                       compiled.Type = firstType(schema)
-                       compiled.Enum = decodeEnum(schema.Enum)
-                       if schema.MinLength != nil {
-                               value := int(*schema.MinLength)
-                               compiled.MinLength = &value
-                       }
-                       if schema.MaxLength != nil {
-                               value := int(*schema.MaxLength)
-                               compiled.MaxLength = &value
-                       }
-                       if schema.Minimum != nil {
-                               value := *schema.Minimum
-                               compiled.Minimum = &value
-                       }
-                       if schema.Maximum != nil {
-                               value := *schema.Maximum
-                               compiled.Maximum = &value
-                       }
-               }
-       }
-       return compiled
-}
-
-func compileRequestBody(body *v3.RequestBody) *BodyValidation {
-       if body == nil {
-               return nil
-       }
-
-       compiled := &BodyValidation{}
-       if body.Required != nil {
-               compiled.Required = *body.Required
-       }
-       if body.Content == nil {
-               return compiled
-       }
-
-       mediaType := body.Content.GetOrZero("application/json")
-       if mediaType == nil || mediaType.Schema == nil {
-               return compiled
-       }
-
-       schema, err := mediaType.Schema.BuildSchema()
-       if err != nil || schema == nil {
-               return compiled
-       }
-       compiled.Schema = compileSchema(schema)
-       return compiled
-}
-
-func compileSchema(schema *base.Schema) *SchemaValidation {
-       if schema == nil {
-               return nil
-       }
-
-       compiled := &SchemaValidation{
-               Type:     firstType(schema),
-               Required: append([]string(nil), schema.Required...),
-               Enum:     decodeEnum(schema.Enum),
-       }
-
-       if schema.MinLength != nil {
-               value := int(*schema.MinLength)
-               compiled.MinLength = &value
-       }
-       if schema.MaxLength != nil {
-               value := int(*schema.MaxLength)
-               compiled.MaxLength = &value
-       }
-       if schema.Minimum != nil {
-               value := *schema.Minimum
-               compiled.Minimum = &value
-       }
-       if schema.Maximum != nil {
-               value := *schema.Maximum
-               compiled.Maximum = &value
-       }
-       if schema.Properties != nil {
-               compiled.Properties = make(map[string]*SchemaValidation, 
schema.Properties.Len())
-               for name, property := range schema.Properties.FromOldest() {
-                       if property == nil {
-                               continue
-                       }
-                       propertySchema, err := property.BuildSchema()
-                       if err != nil || propertySchema == nil {
-                               continue
-                       }
-                       compiled.Properties[name] = 
compileSchema(propertySchema)
-               }
-       }
-
-       return compiled
-}
-
-func firstType(schema *base.Schema) string {
-       if schema == nil || len(schema.Type) == 0 {
-               return ""
-       }
-       return schema.Type[0]
-}
-
-func normalizeOpenAPIPath(path string) string {
-       return openAPIPathParamPattern.ReplaceAllString(path, `:$1`)
-}
-
-func decodeEnum(values []*yaml.Node) []string {
-       if len(values) == 0 {
-               return nil
-       }
-
-       result := make([]string, 0, len(values))
-       for _, value := range values {
-               if value == nil {
-                       continue
-               }
-               var decoded string
-               if err := value.Decode(&decoded); err == nil {
-                       result = append(result, decoded)
-                       continue
-               }
-               result = append(result, value.Value)
-       }
-       return result
-}
diff --git a/pkg/filter/http/apiconfig/openapi/compiler_test.go 
b/pkg/filter/http/apiconfig/openapi/compiler_test.go
deleted file mode 100644
index e43a51080..000000000
--- a/pkg/filter/http/apiconfig/openapi/compiler_test.go
+++ /dev/null
@@ -1,203 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-import (
-       "net/http"
-       "testing"
-)
-
-import (
-       "github.com/stretchr/testify/assert"
-       "github.com/stretchr/testify/require"
-)
-
-const minimalOpenAPISpec = `
-openapi: 3.0.3
-info:
-  title: users
-  version: "1.0.0"
-paths:
-  /users/{id}:
-    get:
-      parameters:
-        - name: id
-          in: path
-          required: true
-          schema:
-            type: string
-        - name: trace-id
-          in: header
-          required: true
-          schema:
-            type: string
-      responses:
-        "200":
-          description: ok
-  /users:
-    post:
-      parameters:
-        - name: source
-          in: query
-          required: true
-          schema:
-            type: string
-            enum:
-              - web
-              - app
-        - name: page
-          in: query
-          required: true
-          schema:
-            type: integer
-            minimum: 1
-            maximum: 100
-        - name: x-trace
-          in: header
-          required: true
-          schema:
-            type: string
-            minLength: 4
-            maxLength: 12
-      requestBody:
-        required: true
-        content:
-          application/json:
-            schema:
-              type: object
-              required:
-                - name
-                - role
-              properties:
-                name:
-                  type: string
-                  maxLength: 32
-                role:
-                  type: string
-                  enum:
-                    - admin
-                    - member
-      responses:
-        "200":
-          description: ok
-`
-
-const pathLevelParametersSpec = `
-openapi: 3.0.3
-info:
-  title: users
-  version: "1.0.0"
-paths:
-  /users/{id}:
-    parameters:
-      - name: id
-        in: path
-        required: true
-        schema:
-          type: string
-      - name: trace-id
-        in: header
-        required: true
-        schema:
-          type: string
-    get:
-      responses:
-        "200":
-          description: ok
-`
-
-func TestCompileOpenAPIRoute(t *testing.T) {
-       doc, err := LoadDocumentFromBytes([]byte(minimalOpenAPISpec))
-       require.NoError(t, err)
-
-       compiled, err := CompileRoutes(doc)
-       require.NoError(t, err)
-       require.Len(t, compiled, 2)
-
-       compiledByMethod := map[string]CompiledRoute{}
-       for _, item := range compiled {
-               compiledByMethod[item.Route.HTTPVerb+" "+item.Route.URLPattern] 
= item
-       }
-
-       getRoute, ok := compiledByMethod[http.MethodGet+" /users/:id"]
-       require.True(t, ok)
-       assert.Equal(t, "/users/:id", getRoute.Route.URLPattern)
-       require.NotNil(t, getRoute.Validation)
-       assert.Equal(t, "/users/:id", getRoute.Validation.RoutePattern)
-       require.Len(t, getRoute.Validation.PathParameters, 1)
-       assert.Equal(t, "id", getRoute.Validation.PathParameters[0].Name)
-       require.Len(t, getRoute.Validation.HeaderParameters, 1)
-       assert.Equal(t, "trace-id", 
getRoute.Validation.HeaderParameters[0].Name)
-       assert.True(t, getRoute.Validation.HeaderParameters[0].Required)
-
-       postRoute, ok := compiledByMethod[http.MethodPost+" /users"]
-       require.True(t, ok)
-       assert.Equal(t, "/users", postRoute.Route.URLPattern)
-       assert.NotNil(t, postRoute.Validation)
-       require.Len(t, postRoute.Validation.QueryParameters, 2)
-       assert.Equal(t, "source", postRoute.Validation.QueryParameters[0].Name)
-       assert.True(t, postRoute.Validation.QueryParameters[0].Required)
-       assert.Equal(t, []string{"web", "app"}, 
postRoute.Validation.QueryParameters[0].Enum)
-       pageParam := postRoute.Validation.QueryParameters[1]
-       assert.Equal(t, "page", pageParam.Name)
-       assert.Equal(t, "integer", pageParam.Type)
-       require.NotNil(t, pageParam.Minimum)
-       assert.Equal(t, 1.0, *pageParam.Minimum)
-       require.NotNil(t, pageParam.Maximum)
-       assert.Equal(t, 100.0, *pageParam.Maximum)
-
-       require.Len(t, postRoute.Validation.HeaderParameters, 1)
-       headerParam := postRoute.Validation.HeaderParameters[0]
-       assert.Equal(t, "x-trace", headerParam.Name)
-       assert.Equal(t, "string", headerParam.Type)
-       require.NotNil(t, headerParam.MinLength)
-       assert.Equal(t, 4, *headerParam.MinLength)
-       require.NotNil(t, headerParam.MaxLength)
-       assert.Equal(t, 12, *headerParam.MaxLength)
-
-       require.NotNil(t, postRoute.Validation.RequestBody)
-       assert.True(t, postRoute.Validation.RequestBody.Required)
-       require.NotNil(t, postRoute.Validation.RequestBody.Schema)
-       assert.Equal(t, "object", postRoute.Validation.RequestBody.Schema.Type)
-       assert.Equal(t, []string{"name", "role"}, 
postRoute.Validation.RequestBody.Schema.Required)
-       require.Contains(t, postRoute.Validation.RequestBody.Schema.Properties, 
"name")
-       require.Contains(t, postRoute.Validation.RequestBody.Schema.Properties, 
"role")
-       require.NotNil(t, 
postRoute.Validation.RequestBody.Schema.Properties["name"].MaxLength)
-       assert.Equal(t, 32, 
*postRoute.Validation.RequestBody.Schema.Properties["name"].MaxLength)
-       assert.Equal(t, []string{"admin", "member"}, 
postRoute.Validation.RequestBody.Schema.Properties["role"].Enum)
-}
-
-func TestCompileOpenAPIRoute_UsesPathLevelParameters(t *testing.T) {
-       doc, err := LoadDocumentFromBytes([]byte(pathLevelParametersSpec))
-       require.NoError(t, err)
-
-       compiled, err := CompileRoutes(doc)
-       require.NoError(t, err)
-       require.Len(t, compiled, 1)
-
-       getRoute := compiled[0]
-       assert.Equal(t, http.MethodGet, getRoute.Route.HTTPVerb)
-       assert.Equal(t, "/users/:id", getRoute.Route.URLPattern)
-       require.NotNil(t, getRoute.Validation)
-       require.Len(t, getRoute.Validation.PathParameters, 1)
-       assert.Equal(t, "id", getRoute.Validation.PathParameters[0].Name)
-       assert.True(t, getRoute.Validation.PathParameters[0].Required)
-       require.Len(t, getRoute.Validation.HeaderParameters, 1)
-       assert.Equal(t, "trace-id", 
getRoute.Validation.HeaderParameters[0].Name)
-       assert.True(t, getRoute.Validation.HeaderParameters[0].Required)
-}
diff --git a/pkg/filter/http/apiconfig/openapi/doc.go 
b/pkg/filter/http/apiconfig/openapi/doc.go
deleted file mode 100644
index b9bc011fc..000000000
--- a/pkg/filter/http/apiconfig/openapi/doc.go
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * 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.
- */
-
-// Package openapi provides OpenAPI parsing and compilation helpers for the
-// api config filter.
-package openapi
diff --git a/pkg/filter/http/apiconfig/openapi/metadata.go 
b/pkg/filter/http/apiconfig/openapi/metadata.go
deleted file mode 100644
index 8e62c8d85..000000000
--- a/pkg/filter/http/apiconfig/openapi/metadata.go
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-import (
-       "github.com/apache/dubbo-go-pixiu/pkg/router"
-)
-
-func ExtractValidationPlan(route router.API) *ValidationPlan {
-       if route.Metadata == nil {
-               return nil
-       }
-       raw, ok := route.Metadata[ValidationPlanMetadataKey]
-       if !ok {
-               return nil
-       }
-       plan, ok := raw.(*ValidationPlan)
-       if !ok {
-               return nil
-       }
-       return plan
-}
diff --git a/pkg/filter/http/apiconfig/openapi/model.go 
b/pkg/filter/http/apiconfig/openapi/model.go
deleted file mode 100644
index afbf62a18..000000000
--- a/pkg/filter/http/apiconfig/openapi/model.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-const ValidationPlanMetadataKey = "openapi.validation.plan"
-
-type ValidationPlan struct {
-       RoutePattern     string
-       PathParameters   []ParameterValidation
-       QueryParameters  []ParameterValidation
-       HeaderParameters []ParameterValidation
-       RequestBody      *BodyValidation
-}
-
-type ParameterValidation struct {
-       Name      string
-       Required  bool
-       Enum      []string
-       Type      string
-       MinLength *int
-       MaxLength *int
-       Minimum   *float64
-       Maximum   *float64
-}
-
-type BodyValidation struct {
-       Required bool
-       Schema   *SchemaValidation
-}
-
-type SchemaValidation struct {
-       Type       string
-       Required   []string
-       Properties map[string]*SchemaValidation
-       Enum       []string
-       MinLength  *int
-       MaxLength  *int
-       Minimum    *float64
-       Maximum    *float64
-}
diff --git a/pkg/filter/http/apiconfig/openapi/validator.go 
b/pkg/filter/http/apiconfig/openapi/validator.go
deleted file mode 100644
index 5296ce777..000000000
--- a/pkg/filter/http/apiconfig/openapi/validator.go
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-import (
-       "bytes"
-       "encoding/json"
-       "fmt"
-       "io"
-       "math"
-       "net/http"
-       "strconv"
-       "strings"
-)
-
-import (
-       "github.com/apache/dubbo-go-pixiu/pkg/router"
-)
-
-func ValidateRequest(req *http.Request, plan *ValidationPlan) error {
-       if req == nil || plan == nil {
-               return nil
-       }
-
-       if err := validatePathParameters(req, plan); err != nil {
-               return err
-       }
-
-       if err := validateQueryParameters(req, plan.QueryParameters); err != 
nil {
-               return err
-       }
-
-       if err := validateHeaderParameters(req, plan.HeaderParameters); err != 
nil {
-               return err
-       }
-
-       if err := validateRequestBody(req, plan.RequestBody); err != nil {
-               return err
-       }
-
-       return nil
-}
-
-func validatePathParameters(req *http.Request, plan *ValidationPlan) error {
-       if plan.RoutePattern == "" || len(plan.PathParameters) == 0 {
-               return nil
-       }
-
-       values := router.GetURIParams(&router.API{URLPattern: 
plan.RoutePattern}, *req.URL)
-       for _, param := range plan.PathParameters {
-               value := values.Get(param.Name)
-               if param.Required && strings.TrimSpace(value) == "" {
-                       return fmt.Errorf("path parameter %q is required", 
param.Name)
-               }
-               if value == "" {
-                       continue
-               }
-               if err := validateParameterValue("path parameter", value, 
param); err != nil {
-                       return err
-               }
-       }
-
-       return nil
-}
-
-func validateQueryParameters(req *http.Request, params []ParameterValidation) 
error {
-       query := req.URL.Query()
-       for _, param := range params {
-               value := query.Get(param.Name)
-               if param.Required && strings.TrimSpace(value) == "" {
-                       return fmt.Errorf("query parameter %q is required", 
param.Name)
-               }
-               if value == "" {
-                       continue
-               }
-               if err := validateParameterValue("query parameter", value, 
param); err != nil {
-                       return err
-               }
-       }
-       return nil
-}
-
-func validateHeaderParameters(req *http.Request, params []ParameterValidation) 
error {
-       for _, param := range params {
-               value := req.Header.Get(param.Name)
-               if param.Required && strings.TrimSpace(value) == "" {
-                       return fmt.Errorf("header %q is required", param.Name)
-               }
-               if value == "" {
-                       continue
-               }
-               if err := validateParameterValue("header", value, param); err 
!= nil {
-                       return err
-               }
-       }
-       return nil
-}
-
-func validateParameterValue(kind string, value string, param 
ParameterValidation) error {
-       if len(param.Enum) > 0 && !contains(param.Enum, value) {
-               return fmt.Errorf("%s %q must be one of %v", kind, param.Name, 
param.Enum)
-       }
-
-       switch param.Type {
-       case "", "string":
-               if param.MinLength != nil && len(value) < *param.MinLength {
-                       return fmt.Errorf("%s %q length must be >= %d", kind, 
param.Name, *param.MinLength)
-               }
-               if param.MaxLength != nil && len(value) > *param.MaxLength {
-                       return fmt.Errorf("%s %q length must be <= %d", kind, 
param.Name, *param.MaxLength)
-               }
-       case "integer":
-               number, err := strconv.ParseInt(value, 10, 64)
-               if err != nil {
-                       return fmt.Errorf("%s %q must be an integer", kind, 
param.Name)
-               }
-               if param.Minimum != nil && float64(number) < *param.Minimum {
-                       return fmt.Errorf("%s %q must be >= %v", kind, 
param.Name, *param.Minimum)
-               }
-               if param.Maximum != nil && float64(number) > *param.Maximum {
-                       return fmt.Errorf("%s %q must be <= %v", kind, 
param.Name, *param.Maximum)
-               }
-       case "number":
-               number, err := strconv.ParseFloat(value, 64)
-               if err != nil {
-                       return fmt.Errorf("%s %q must be a number", kind, 
param.Name)
-               }
-               if param.Minimum != nil && number < *param.Minimum {
-                       return fmt.Errorf("%s %q must be >= %v", kind, 
param.Name, *param.Minimum)
-               }
-               if param.Maximum != nil && number > *param.Maximum {
-                       return fmt.Errorf("%s %q must be <= %v", kind, 
param.Name, *param.Maximum)
-               }
-       case "boolean":
-               if _, err := strconv.ParseBool(value); err != nil {
-                       return fmt.Errorf("%s %q must be a boolean", kind, 
param.Name)
-               }
-       }
-
-       return nil
-}
-
-func validateRequestBody(req *http.Request, body *BodyValidation) error {
-       if body == nil {
-               return nil
-       }
-
-       raw, err := io.ReadAll(req.Body)
-       if err != nil {
-               return fmt.Errorf("read request body: %w", err)
-       }
-       req.Body = io.NopCloser(bytes.NewReader(raw))
-
-       if body.Required && len(bytes.TrimSpace(raw)) == 0 {
-               return fmt.Errorf("request body is required")
-       }
-       if len(bytes.TrimSpace(raw)) == 0 || body.Schema == nil {
-               return nil
-       }
-
-       if !strings.Contains(strings.ToLower(req.Header.Get("Content-Type")), 
"application/json") {
-               return fmt.Errorf("request body content-type must be 
application/json")
-       }
-
-       var payload any
-       if err := json.Unmarshal(raw, &payload); err != nil {
-               return fmt.Errorf("request body must be valid json: %w", err)
-       }
-
-       return validateSchema("$", payload, body.Schema)
-}
-
-func validateSchema(path string, value any, schema *SchemaValidation) error {
-       if schema == nil {
-               return nil
-       }
-
-       switch schema.Type {
-       case "object":
-               obj, ok := value.(map[string]any)
-               if !ok {
-                       return fmt.Errorf("%s must be an object", path)
-               }
-               for _, field := range schema.Required {
-                       if _, exists := obj[field]; !exists {
-                               return fmt.Errorf("%s.%s is required", path, 
field)
-                       }
-               }
-               for key, child := range schema.Properties {
-                       childValue, exists := obj[key]
-                       if !exists {
-                               continue
-                       }
-                       if err := validateSchema(path+"."+key, childValue, 
child); err != nil {
-                               return err
-                       }
-               }
-       case "string":
-               str, ok := value.(string)
-               if !ok {
-                       return fmt.Errorf("%s must be a string", path)
-               }
-               if schema.MinLength != nil && len(str) < *schema.MinLength {
-                       return fmt.Errorf("%s length must be >= %d", path, 
*schema.MinLength)
-               }
-               if schema.MaxLength != nil && len(str) > *schema.MaxLength {
-                       return fmt.Errorf("%s length must be <= %d", path, 
*schema.MaxLength)
-               }
-               if len(schema.Enum) > 0 && !contains(schema.Enum, str) {
-                       return fmt.Errorf("%s must be one of %v", path, 
schema.Enum)
-               }
-       case "number":
-               number, ok := toFloat64(value)
-               if !ok {
-                       return fmt.Errorf("%s must be a number", path)
-               }
-               if schema.Minimum != nil && number < *schema.Minimum {
-                       return fmt.Errorf("%s must be >= %v", path, 
*schema.Minimum)
-               }
-               if schema.Maximum != nil && number > *schema.Maximum {
-                       return fmt.Errorf("%s must be <= %v", path, 
*schema.Maximum)
-               }
-       case "integer":
-               number, ok := toFloat64(value)
-               if !ok || math.Mod(number, 1) != 0 {
-                       return fmt.Errorf("%s must be an integer", path)
-               }
-               if schema.Minimum != nil && number < *schema.Minimum {
-                       return fmt.Errorf("%s must be >= %v", path, 
*schema.Minimum)
-               }
-               if schema.Maximum != nil && number > *schema.Maximum {
-                       return fmt.Errorf("%s must be <= %v", path, 
*schema.Maximum)
-               }
-       }
-
-       return nil
-}
-
-func contains(values []string, candidate string) bool {
-       for _, value := range values {
-               if value == candidate {
-                       return true
-               }
-       }
-       return false
-}
-
-func toFloat64(value any) (float64, bool) {
-       switch typed := value.(type) {
-       case float64:
-               return typed, true
-       case float32:
-               return float64(typed), true
-       case int:
-               return float64(typed), true
-       case int8:
-               return float64(typed), true
-       case int16:
-               return float64(typed), true
-       case int32:
-               return float64(typed), true
-       case int64:
-               return float64(typed), true
-       case uint:
-               return float64(typed), true
-       case uint8:
-               return float64(typed), true
-       case uint16:
-               return float64(typed), true
-       case uint32:
-               return float64(typed), true
-       case uint64:
-               return float64(typed), true
-       default:
-               return 0, false
-       }
-}
diff --git a/pkg/filter/http/apiconfig/openapi/validator_test.go 
b/pkg/filter/http/apiconfig/openapi/validator_test.go
deleted file mode 100644
index b2d57ef55..000000000
--- a/pkg/filter/http/apiconfig/openapi/validator_test.go
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * 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.
- */
-
-package openapi
-
-import (
-       "net/http"
-       "net/http/httptest"
-       "strings"
-       "testing"
-)
-
-import (
-       "github.com/stretchr/testify/assert"
-       "github.com/stretchr/testify/require"
-)
-
-func TestValidateRequest_MissingRequiredQuery(t *testing.T) {
-       req := httptest.NewRequest("GET", "/users", nil)
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               QueryParameters: []ParameterValidation{
-                       {Name: "id", Required: true},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "id")
-}
-
-func TestValidateRequest_QueryEnumMismatch(t *testing.T) {
-       req := httptest.NewRequest("GET", "/users?role=superadmin", nil)
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               QueryParameters: []ParameterValidation{
-                       {Name: "role", Enum: []string{"admin", "member"}},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "role")
-}
-
-func TestValidateRequest_QueryTypeMismatch(t *testing.T) {
-       req := httptest.NewRequest(http.MethodGet, "/users?page=abc", nil)
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               QueryParameters: []ParameterValidation{
-                       {Name: "page", Type: "integer", Required: true},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "integer")
-}
-
-func TestValidateRequest_QueryRangeViolation(t *testing.T) {
-       req := httptest.NewRequest(http.MethodGet, "/users?page=0", nil)
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               QueryParameters: []ParameterValidation{
-                       {Name: "page", Type: "integer", Minimum: float64Ptr(1), 
Maximum: float64Ptr(100)},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, ">=")
-}
-
-func TestValidateRequest_InvalidJSONBody(t *testing.T) {
-       req := httptest.NewRequest("POST", "/users", 
strings.NewReader(`{"role":"member"}`))
-       req.Header.Set("Content-Type", "application/json")
-
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               RequestBody: &BodyValidation{
-                       Required: true,
-                       Schema: &SchemaValidation{
-                               Type:     "object",
-                               Required: []string{"name", "role"},
-                               Properties: map[string]*SchemaValidation{
-                                       "name": {
-                                               Type:      "string",
-                                               MaxLength: intPtr(32),
-                                       },
-                                       "role": {
-                                               Type: "string",
-                                               Enum: []string{"admin", 
"member"},
-                                       },
-                               },
-                       },
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "name")
-}
-
-func TestValidateRequest_MissingRequiredHeader(t *testing.T) {
-       req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
-       plan := &ValidationPlan{
-               RoutePattern: "/users/:id",
-               PathParameters: []ParameterValidation{
-                       {Name: "id", Required: true},
-               },
-               HeaderParameters: []ParameterValidation{
-                       {Name: "trace-id", Required: true},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "trace-id")
-}
-
-func TestValidateRequest_HeaderLengthViolation(t *testing.T) {
-       req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
-       req.Header.Set("trace-id", "abc")
-       plan := &ValidationPlan{
-               RoutePattern: "/users/:id",
-               HeaderParameters: []ParameterValidation{
-                       {Name: "trace-id", Type: "string", MinLength: 
intPtr(4)},
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "length")
-}
-
-func TestValidateRequest_InvalidNumericBody(t *testing.T) {
-       req := httptest.NewRequest(http.MethodPost, "/products", 
strings.NewReader(`{"price":-10}`))
-       req.Header.Set("Content-Type", "application/json")
-
-       plan := &ValidationPlan{
-               RoutePattern: "/products",
-               RequestBody: &BodyValidation{
-                       Required: true,
-                       Schema: &SchemaValidation{
-                               Type:     "object",
-                               Required: []string{"price"},
-                               Properties: map[string]*SchemaValidation{
-                                       "price": {
-                                               Type:    "number",
-                                               Minimum: float64Ptr(0),
-                                       },
-                               },
-                       },
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "price")
-}
-
-func TestValidateRequest_RejectsUnsupportedBodyContentType(t *testing.T) {
-       req := httptest.NewRequest(http.MethodPost, "/users", 
strings.NewReader(`{"name":"tom"}`))
-       req.Header.Set("Content-Type", "text/plain")
-
-       plan := &ValidationPlan{
-               RoutePattern: "/users",
-               RequestBody: &BodyValidation{
-                       Required: true,
-                       Schema: &SchemaValidation{
-                               Type: "object",
-                       },
-               },
-       }
-
-       err := ValidateRequest(req, plan)
-       require.Error(t, err)
-       assert.ErrorContains(t, err, "application/json")
-}
-
-func float64Ptr(v float64) *float64 {
-       return &v
-}
-
-func intPtr(v int) *int {
-       return &v
-}
diff --git a/pkg/filter/http/openapi/openapi.go 
b/pkg/filter/http/openapi/openapi.go
new file mode 100644
index 000000000..170b17b3b
--- /dev/null
+++ b/pkg/filter/http/openapi/openapi.go
@@ -0,0 +1,567 @@
+/*
+ * 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.
+ */
+
+package openapi
+
+import (
+       "bytes"
+       "io"
+       "net/http"
+       "net/url"
+       "os"
+       "path/filepath"
+       "strings"
+)
+
+import (
+       "github.com/pb33f/libopenapi"
+       openapiValidator "github.com/pb33f/libopenapi-validator"
+       validatorConfig "github.com/pb33f/libopenapi-validator/config"
+       validatorErrors "github.com/pb33f/libopenapi-validator/errors"
+       validatorPaths "github.com/pb33f/libopenapi-validator/paths"
+       validatorRadix "github.com/pb33f/libopenapi-validator/radix"
+       "github.com/pb33f/libopenapi/datamodel"
+       v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+
+       "github.com/pkg/errors"
+
+       "gopkg.in/yaml.v3"
+)
+
+import (
+       "github.com/apache/dubbo-go-pixiu/pkg/common/constant"
+       "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+       contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
+       "github.com/apache/dubbo-go-pixiu/pkg/logger"
+)
+
+const (
+       // Kind is the kind of OpenAPI validation filter.
+       Kind = constant.HTTPOpenAPIFilter
+
+       defaultMaxRequestBodyBytes = 1 << 20
+)
+
+var errOpenAPIRequestBodyTooLarge = errors.New("openapi request body too 
large")
+
+func init() {
+       filter.RegisterHttpFilter(&Plugin{})
+}
+
+type (
+       Plugin struct {
+       }
+
+       FilterFactory struct {
+               cfg               *Config
+               validator         openapiValidator.Validator
+               model             *v3.Document
+               validationOptions *validatorConfig.ValidationOptions
+               maxRequestBody    int64
+       }
+
+       Filter struct {
+               validator         openapiValidator.Validator
+               model             *v3.Document
+               validationOptions *validatorConfig.ValidationOptions
+               maxRequestBody    int64
+       }
+
+       Config struct {
+               Path string `yaml:"path" json:"path,omitempty"`
+               // MaxRequestBodyBytes limits how much request body data 
OpenAPI validation may read.
+               // Zero uses the default limit.
+               MaxRequestBodyBytes int64 `yaml:"max_request_body_bytes" 
json:"max_request_body_bytes,omitempty"`
+       }
+)
+
+func (p *Plugin) Kind() string {
+       return Kind
+}
+
+func (p *Plugin) CreateFilterFactory() (filter.HttpFilterFactory, error) {
+       return &FilterFactory{cfg: &Config{}}, nil
+}
+
+func (factory *FilterFactory) Config() any {
+       return factory.cfg
+}
+
+func (factory *FilterFactory) Apply() error {
+       path, err := cleanOpenAPIPath(factory.cfg.Path)
+       if err != nil {
+               return err
+       }
+       factory.cfg.Path = path
+
+       maxRequestBody, err := factory.cfg.effectiveMaxRequestBodyBytes()
+       if err != nil {
+               return err
+       }
+
+       validator, model, validationOptions, err := loadValidatorFromFile(path)
+       if err != nil {
+               return err
+       }
+       factory.validator = validator
+       factory.model = model
+       factory.validationOptions = validationOptions
+       factory.maxRequestBody = maxRequestBody
+       return nil
+}
+
+func (factory *FilterFactory) PrepareFilterChain(ctx *contexthttp.HttpContext, 
chain filter.FilterChain) error {
+       f := &Filter{
+               validator:         factory.validator,
+               model:             factory.model,
+               validationOptions: factory.validationOptions,
+               maxRequestBody:    factory.maxRequestBody,
+       }
+       chain.AppendDecodeFilters(f)
+       return nil
+}
+
+func (f *Filter) Decode(ctx *contexthttp.HttpContext) filter.FilterStatus {
+       if f.validator == nil || f.model == nil {
+               return filter.Continue
+       }
+
+       req := ctx.Request
+       pathItem, foundPath, ok := f.findRequestOperation(req)
+       if !ok {
+               return filter.Continue
+       }
+
+       if err := f.prepareRequestBodyForValidation(req); err != nil {
+               if err == errOpenAPIRequestBodyTooLarge {
+                       errResp := contexthttp.PayloadTooLarge.WithError(err)
+                       ctx.SendLocalReply(errResp.Status, errResp.ToJSON())
+                       logger.Debug(errResp.Error())
+                       return filter.Stop
+               }
+               errResp := contexthttp.BadRequest.WithError(errors.New("openapi 
request body read failed"))
+               ctx.SendLocalReply(errResp.Status, errResp.ToJSON())
+               logger.Debugf("openapi request body read failed: %v", err)
+               return filter.Stop
+       }
+
+       if valid, validationErrs := 
f.validator.ValidateHttpRequestSyncWithPathItem(req, pathItem, foundPath); 
!valid {
+               validationDetails := formatValidationErrors(validationErrs)
+               errResp := contexthttp.BadRequest.WithError(errors.New("openapi 
request validation failed"))
+               ctx.SendLocalReply(errResp.Status, errResp.ToJSON())
+               logger.Debugf("openapi request validation failed: %s", 
validationDetails)
+               return filter.Stop
+       }
+       return filter.Continue
+}
+
+func (f *Filter) findRequestOperation(req *http.Request) (*v3.PathItem, 
string, bool) {
+       pathItem, validationErrs, foundPath := validatorPaths.FindPath(req, 
f.model, f.validationOptions)
+       if len(validationErrs) > 0 {
+               logger.Debugf("openapi path lookup errors for %s %s: %s", 
req.Method, req.URL.Path, formatValidationErrors(validationErrs))
+               return nil, "", false
+       }
+       if pathItem == nil {
+               return nil, "", false
+       }
+       if !hasRequestOperation(req, pathItem) {
+               return nil, "", false
+       }
+       return pathItem, foundPath, true
+}
+
+func hasRequestOperation(req *http.Request, pathItem *v3.PathItem) bool {
+       switch req.Method {
+       case http.MethodGet:
+               return pathItem.Get != nil
+       case http.MethodPost:
+               return pathItem.Post != nil
+       case http.MethodPut:
+               return pathItem.Put != nil
+       case http.MethodDelete:
+               return pathItem.Delete != nil
+       case http.MethodOptions:
+               return pathItem.Options != nil
+       case http.MethodHead:
+               return pathItem.Head != nil
+       case http.MethodPatch:
+               return pathItem.Patch != nil
+       case http.MethodTrace:
+               return pathItem.Trace != nil
+       default:
+               operations := pathItem.GetOperations()
+               return operations != nil && 
operations.GetOrZero(strings.ToLower(req.Method)) != nil
+       }
+}
+
+func (f *Filter) prepareRequestBodyForValidation(req *http.Request) error {
+       if req.Body == nil || req.Body == http.NoBody {
+               return nil
+       }
+       if f.maxRequestBody <= 0 {
+               return nil
+       }
+       if req.ContentLength > f.maxRequestBody {
+               return errOpenAPIRequestBodyTooLarge
+       }
+
+       body, err := readRequestBodyWithinLimit(req.Body, f.maxRequestBody)
+       if err != nil {
+               return err
+       }
+       req.Body = io.NopCloser(bytes.NewReader(body))
+       req.ContentLength = int64(len(body))
+       return nil
+}
+
+func readRequestBodyWithinLimit(body io.ReadCloser, limit int64) ([]byte, 
error) {
+       // The original body is replaced by the caller after this bounded read.
+       defer body.Close()
+
+       data, err := io.ReadAll(io.LimitReader(body, limit+1))
+       if err != nil {
+               return nil, err
+       }
+       if int64(len(data)) > limit {
+               return nil, errOpenAPIRequestBodyTooLarge
+       }
+       return data, nil
+}
+
+func loadValidatorFromFile(path string) (openapiValidator.Validator, 
*v3.Document, *validatorConfig.ValidationOptions, error) {
+       if err := validateExternalRefs(path); err != nil {
+               return nil, nil, nil, err
+       }
+
+       spec, err := os.ReadFile(path)
+       if err != nil {
+               return nil, nil, nil, errors.Wrap(err, "read openapi file")
+       }
+
+       doc, err := libopenapi.NewDocumentWithConfiguration(spec, 
&datamodel.DocumentConfiguration{
+               BasePath: filepath.Dir(path),
+       })
+       if err != nil {
+               return nil, nil, nil, errors.Wrap(err, "parse openapi document")
+       }
+
+       model, err := doc.BuildV3Model()
+       if err != nil {
+               return nil, nil, nil, errors.Wrap(err, "build openapi model")
+       }
+
+       validationOptions := 
validatorConfig.NewValidationOptions(validatorConfig.WithoutSecurityValidation())
+       if validationOptions.PathTree == nil && 
!validationOptions.IsPathTreeDisabled() {
+               validationOptions.PathTree = 
validatorRadix.BuildPathTree(&model.Model)
+       }
+
+       validator := openapiValidator.NewValidatorFromV3Model(&model.Model, 
validatorConfig.WithExistingOpts(validationOptions))
+       if validator == nil {
+               return nil, nil, nil, errors.New("load openapi validator: 
validator is nil")
+       }
+       return validator, &model.Model, validationOptions, nil
+}
+
+func cleanOpenAPIPath(path string) (string, error) {
+       path = strings.TrimSpace(path)
+       if path == "" {
+               return "", errors.New("openapi path is required")
+       }
+       if filepath.IsAbs(path) {
+               return "", errors.Errorf("openapi path must be relative: %s", 
path)
+       }
+       if containsParentDirectory(path) {
+               return "", errors.Errorf("openapi path must not contain parent 
directory: %s", path)
+       }
+
+       cleanPath := filepath.Clean(path)
+       if cleanPath == "." {
+               return "", errors.New("openapi path is required")
+       }
+
+       // Resolve symlinks so that a relative path like "specs/current/passwd"
+       // cannot bypass the sensitive-directory check when "specs/current" is
+       // a symlink pointing to /etc.
+       resolved, err := filepath.EvalSymlinks(cleanPath)
+       if err != nil {
+               return "", errors.Wrap(err, "resolve openapi path symlinks")
+       }
+
+       basePath, err := filepath.Abs(filepath.Dir(resolved))
+       if err != nil {
+               return "", errors.Wrap(err, "resolve openapi path base")
+       }
+       if isSensitiveOpenAPIBasePath(basePath) {
+               return "", errors.Errorf("openapi path base directory is not 
allowed: %s", basePath)
+       }
+       return cleanPath, nil
+}
+
+func validateExternalRefs(rootPath string) error {
+       rootPath = filepath.Clean(rootPath)
+       rootDir := filepath.Dir(rootPath)
+       visited := map[string]struct{}{}
+       return validateExternalRefsInFile(rootPath, rootDir, visited)
+}
+
+func validateExternalRefsInFile(path string, rootDir string, visited 
map[string]struct{}) error {
+       path = filepath.Clean(path)
+       if _, ok := visited[path]; ok {
+               return nil
+       }
+       visited[path] = struct{}{}
+
+       spec, err := os.ReadFile(path)
+       if err != nil {
+               return errors.Wrap(err, "read openapi file for ref validation")
+       }
+
+       var raw any
+       if err := yaml.Unmarshal(spec, &raw); err != nil {
+               return errors.Wrap(err, "parse openapi file for ref validation")
+       }
+
+       refs := collectExternalRefs(raw, nil)
+       for _, ref := range refs {
+               if err := validateExternalRefPath(path, rootDir, ref); err != 
nil {
+                       return err
+               }
+
+               refPath, _, ok := splitRefTarget(ref)
+               if !ok {
+                       continue
+               }
+               if filepath.Ext(refPath) == "" && strings.HasSuffix(refPath, 
"/") {
+                       continue
+               }
+               resolvedRefPath, err := 
resolveSafeReferencePath(filepath.Dir(path), refPath, rootDir)
+               if err != nil {
+                       return err
+               }
+               if err := validateExternalRefsInFile(resolvedRefPath, rootDir, 
visited); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func collectExternalRefs(value any, refs []string) []string {
+       switch v := value.(type) {
+       case map[string]any:
+               for key, child := range v {
+                       if key == "$ref" {
+                               if ref, ok := child.(string); ok && ref != "" 
&& !strings.HasPrefix(ref, "#") {
+                                       refs = append(refs, ref)
+                               }
+                               continue
+                       }
+                       refs = collectExternalRefs(child, refs)
+               }
+       case []any:
+               for _, child := range v {
+                       refs = collectExternalRefs(child, refs)
+               }
+       }
+       return refs
+}
+
+func splitRefTarget(ref string) (string, string, bool) {
+       if ref == "" {
+               return "", "", false
+       }
+       if strings.HasPrefix(ref, "#") {
+               return "", ref, false
+       }
+       parsed, err := url.Parse(ref)
+       if err != nil {
+               return "", "", false
+       }
+       if parsed.Scheme != "" && parsed.Scheme != "file" {
+               return "", "", false
+       }
+       target := parsed.Path
+       if target == "" {
+               target = parsed.Opaque
+       }
+       fragment := parsed.Fragment
+       if idx := strings.Index(target, "#"); idx >= 0 {
+               fragment = target[idx+1:]
+               target = target[:idx]
+       }
+       return target, fragment, true
+}
+
+func validateExternalRefPath(currentFile string, rootDir string, ref string) 
error {
+       if ref == "" || strings.HasPrefix(ref, "#") {
+               return nil
+       }
+       parsed, err := url.Parse(ref)
+       if err != nil {
+               return errors.Wrap(err, "parse openapi external ref")
+       }
+       if parsed.Scheme != "" || parsed.Host != "" {
+               return errors.Errorf("openapi external ref must be relative: 
%s", ref)
+       }
+
+       refPath, _, ok := splitRefTarget(ref)
+       if !ok {
+               return nil
+       }
+       if filepath.IsAbs(refPath) {
+               return errors.Errorf("openapi external ref must be relative: 
%s", ref)
+       }
+       if containsParentDirectory(refPath) {
+               return errors.Errorf("openapi external ref must not contain 
parent directory: %s", ref)
+       }
+       _, err = resolveSafeReferencePath(filepath.Dir(currentFile), refPath, 
rootDir)
+       return err
+}
+
+func resolveSafeReferencePath(baseDir string, refPath string, rootDir string) 
(string, error) {
+       candidate := filepath.Clean(filepath.Join(baseDir, refPath))
+       resolved, err := filepath.EvalSymlinks(candidate)
+       if err != nil {
+               return "", errors.Wrap(err, "resolve openapi external ref 
symlinks")
+       }
+       if !isWithinBaseDir(resolved, rootDir) {
+               return "", errors.Errorf("openapi external ref escapes allowed 
directory: %s", resolved)
+       }
+       return resolved, nil
+}
+
+func isWithinBaseDir(path string, baseDir string) bool {
+       path = filepath.Clean(path)
+       baseDir = filepath.Clean(baseDir)
+       if path == baseDir {
+               return true
+       }
+       rel, err := filepath.Rel(baseDir, path)
+       if err != nil {
+               return false
+       }
+       return rel != ".." && !strings.HasPrefix(rel, 
".."+string(filepath.Separator))
+}
+
+func (c *Config) effectiveMaxRequestBodyBytes() (int64, error) {
+       if c.MaxRequestBodyBytes < 0 {
+               return 0, errors.New("max_request_body_bytes must not be 
negative")
+       }
+       if c.MaxRequestBodyBytes == 0 {
+               return defaultMaxRequestBodyBytes, nil
+       }
+       return c.MaxRequestBodyBytes, nil
+}
+
+func containsParentDirectory(path string) bool {
+       for _, part := range strings.Split(filepath.ToSlash(path), "/") {
+               if part == ".." {
+                       return true
+               }
+       }
+       return false
+}
+
+func isSensitiveOpenAPIBasePath(path string) bool {
+       // Resolve symlinks on the candidate path so that macOS /private/etc
+       // matches against the sensitive entry /etc.
+       resolved := resolveExistingAncestor(path)
+
+       if resolved == filepath.Clean(string(filepath.Separator)) {
+               return true
+       }
+
+       sensitivePaths := []string{"/etc", "/proc", "/sys", "/dev", "/run", 
"/var/run"}
+       for _, sensitivePath := range sensitivePaths {
+               // Resolve symlinks on the sensitive path too (macOS: /etc -> 
/private/etc).
+               resolvedSensitive := resolveExistingAncestor(sensitivePath)
+               if isPathWithin(resolved, resolvedSensitive) {
+                       return true
+               }
+       }
+       return false
+}
+
+// resolveExistingAncestor resolves symlinks on the longest existing ancestor
+// of path, then appends any remaining non-existent components. This avoids
+// EvalSymlinks failing on paths whose leaf does not yet exist on disk.
+func resolveExistingAncestor(path string) string {
+       path = filepath.Clean(path)
+
+       // Walk up until we find an existing component.
+       candidate := path
+       var tail []string
+       for {
+               _, err := os.Lstat(candidate)
+               if err == nil {
+                       break
+               }
+               parent := filepath.Dir(candidate)
+               if parent == candidate {
+                       // Reached root without finding an existing component.
+                       return path
+               }
+               tail = append([]string{filepath.Base(candidate)}, tail...)
+               candidate = parent
+       }
+
+       resolved, err := filepath.EvalSymlinks(candidate)
+       if err != nil {
+               return path
+       }
+       if len(tail) == 0 {
+               return resolved
+       }
+       return filepath.Join(append([]string{resolved}, tail...)...)
+}
+
+func isPathWithin(path string, base string) bool {
+       base = filepath.Clean(base)
+       if path == base {
+               return true
+       }
+
+       rel, err := filepath.Rel(base, path)
+       if err != nil {
+               return false
+       }
+       return rel != ".." && !strings.HasPrefix(rel, 
".."+string(filepath.Separator))
+}
+
+func formatValidationErrors(errs []*validatorErrors.ValidationError) string {
+       messages := make([]string, 0, len(errs))
+       for _, validationErr := range errs {
+               if validationErr == nil {
+                       continue
+               }
+               switch {
+               case validationErr.Message != "" && validationErr.Reason != "":
+                       messages = append(messages, validationErr.Message+": 
"+validationErr.Reason)
+               case validationErr.Message != "":
+                       messages = append(messages, validationErr.Message)
+               case validationErr.Reason != "":
+                       messages = append(messages, validationErr.Reason)
+               default:
+                       messages = append(messages, validationErr.Error())
+               }
+       }
+       if len(messages) == 0 {
+               return "openapi request validation failed"
+       }
+       return strings.Join(messages, "; ")
+}
+
+var _ filter.HttpFilterFactory = new(FilterFactory)
diff --git a/pkg/filter/http/openapi/openapi_test.go 
b/pkg/filter/http/openapi/openapi_test.go
new file mode 100644
index 000000000..56f167718
--- /dev/null
+++ b/pkg/filter/http/openapi/openapi_test.go
@@ -0,0 +1,834 @@
+/*
+ * 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.
+ */
+
+package openapi
+
+import (
+       "io"
+       "net/http"
+       "net/http/httptest"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+)
+
+import (
+       v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+import (
+       extfilter "github.com/apache/dubbo-go-pixiu/pkg/common/extension/filter"
+       contexthttp "github.com/apache/dubbo-go-pixiu/pkg/context/http"
+)
+
+func TestDecode_StopsOnOpenAPIValidationFailure(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+       req := httptest.NewRequest(http.MethodPost, "/users", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusBadRequest, recorder.Code)
+       assert.True(t, ctx.LocalReply())
+}
+
+func TestDecode_HidesOpenAPIValidationDetailsFromClient(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+       req := httptest.NewRequest(http.MethodPost, "/users", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusBadRequest, recorder.Code)
+       assert.Contains(t, recorder.Body.String(), "openapi request validation 
failed")
+       assert.NotContains(t, recorder.Body.String(), "source")
+}
+
+func TestDecode_RejectsOpenAPIRequestBodyLargerThanLimit(t *testing.T) {
+       filterInstance := newOpenAPIFilterWithConfig(t, 
usersSpecWithQueryAndBody(), func(cfg *Config) {
+               cfg.MaxRequestBodyBytes = 8
+       })
+       req := httptest.NewRequest(http.MethodPost, "/users?source=web", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusRequestEntityTooLarge, recorder.Code)
+       assert.Contains(t, recorder.Body.String(), "openapi request body too 
large")
+}
+
+func TestDecode_RejectsChunkedOpenAPIRequestBodyLargerThanLimit(t *testing.T) {
+       filterInstance := newOpenAPIFilterWithConfig(t, 
usersSpecWithQueryAndBody(), func(cfg *Config) {
+               cfg.MaxRequestBodyBytes = 8
+       })
+       req := httptest.NewRequest(http.MethodPost, "/users?source=web", 
io.NopCloser(strings.NewReader(`{"name":"tom"}`)))
+       req.ContentLength = -1
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusRequestEntityTooLarge, recorder.Code)
+       assert.Contains(t, recorder.Body.String(), "openapi request body too 
large")
+}
+
+func TestDecode_ContinuesOnOpenAPIValidationSuccess(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+       req := httptest.NewRequest(http.MethodPost, "/users?source=web", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Continue, status)
+       body, err := io.ReadAll(req.Body)
+       require.NoError(t, err)
+       assert.JSONEq(t, `{"name":"tom"}`, string(body))
+}
+
+func TestDecode_IgnoresOpenAPISecurityValidation(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, `
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+components:
+  securitySchemes:
+    ApiKeyAuth:
+      type: apiKey
+      in: header
+      name: X-API-Key
+security:
+  - ApiKeyAuth: []
+paths:
+  /users:
+    post:
+      parameters:
+        - name: source
+          in: query
+          required: true
+          schema:
+            type: string
+      responses:
+        "200":
+          description: ok
+`)
+       req := httptest.NewRequest(http.MethodPost, "/users?source=web", nil)
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Continue, status)
+       assert.False(t, ctx.LocalReply())
+}
+
+func TestDecode_SkipsRoutesNotDeclaredInOpenAPI(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+       req := httptest.NewRequest(http.MethodGet, "/orders", nil)
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Continue, status)
+       assert.False(t, ctx.LocalReply())
+}
+
+func TestDecode_SkipsMethodsNotDeclaredInOpenAPI(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, `
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    get:
+      responses:
+        "200":
+          description: ok
+`)
+       req := httptest.NewRequest(http.MethodPost, "/users", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Continue, status)
+       assert.False(t, ctx.LocalReply())
+}
+
+func TestDecode_SkipsHeadWhenOnlyGetDeclared(t *testing.T) {
+       // When only GET is declared (no HEAD), the SDK FindPath treats HEAD as
+       // an undeclared method and returns nil. The filter skips validation and
+       // lets the request continue to downstream filters / handlers.
+       filterInstance := newOpenAPIFilter(t, `
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    get:
+      parameters:
+        - name: source
+          in: query
+          required: true
+          schema:
+            type: string
+            enum: [web, app]
+      responses:
+        "200":
+          description: ok
+`)
+
+       t.Run("HEAD without required query param is skipped", func(t 
*testing.T) {
+               req := httptest.NewRequest(http.MethodHead, "/users", nil)
+               recorder := httptest.NewRecorder()
+               ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+               status := filterInstance.Decode(ctx)
+
+               assert.Equal(t, extfilter.Continue, status)
+               assert.False(t, ctx.LocalReply())
+       })
+
+       t.Run("HEAD with valid query param is also skipped", func(t *testing.T) 
{
+               req := httptest.NewRequest(http.MethodHead, 
"/users?source=web", nil)
+               recorder := httptest.NewRecorder()
+               ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+               status := filterInstance.Decode(ctx)
+
+               assert.Equal(t, extfilter.Continue, status)
+               assert.False(t, ctx.LocalReply())
+       })
+}
+
+func TestHasRequestOperation_HeadRequiresExplicitHeadOperation(t *testing.T) {
+       req := httptest.NewRequest(http.MethodHead, "/users", nil)
+
+       assert.False(t, hasRequestOperation(req, &v3.PathItem{
+               Get: &v3.Operation{},
+       }))
+       assert.True(t, hasRequestOperation(req, &v3.PathItem{
+               Head: &v3.Operation{},
+       }))
+}
+
+func TestApply_RejectsInvalidOpenAPIModel(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.WriteFile("openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/User"
+      responses:
+        "200":
+          description: ok
+components:
+  schemas:
+    User:
+      $ref: "#/components/schemas/User2"
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "build openapi model")
+}
+
+func TestApply_RejectsAbsoluteOpenAPIPath(t *testing.T) {
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "/etc/passwd",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi path must be relative")
+}
+
+func TestApply_RejectsParentDirectoryOpenAPIPath(t *testing.T) {
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "../openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi path must not contain parent 
directory")
+}
+
+func TestApply_RejectsSymlinkPointingToSensitiveDirectory(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+
+       // Create a symlink: specs/current -> /etc
+       require.NoError(t, os.MkdirAll("specs", 0o700))
+       require.NoError(t, os.Symlink("/etc", filepath.Join(dir, "specs", 
"current")))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "specs/current/passwd",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi path base directory is not 
allowed")
+}
+
+func TestApply_AllowsNestedLocalExternalRefs(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.MkdirAll("specs/schemas", 0o700))
+       require.NoError(t, os.WriteFile("specs/openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: ./schemas/user.yaml#/User
+      responses:
+        "200":
+          description: ok
+`), 0o600))
+       require.NoError(t, os.WriteFile("specs/schemas/user.yaml", []byte(`
+User:
+  type: object
+  required: [name]
+  properties:
+    name:
+      $ref: ./name.yaml#/Name
+`), 0o600))
+       require.NoError(t, os.WriteFile("specs/schemas/name.yaml", []byte(`
+Name:
+  type: string
+  minLength: 3
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "specs/openapi.yaml",
+               },
+       }
+
+       require.NoError(t, factory.Apply())
+}
+
+func TestApply_RejectsExternalRefWithParentDirectory(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.MkdirAll("specs", 0o700))
+       require.NoError(t, os.WriteFile("specs/openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: ../schemas/user.yaml#/User
+      responses:
+        "200":
+          description: ok
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "specs/openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi external ref must not contain 
parent directory")
+}
+
+func TestApply_RejectsAbsoluteExternalRef(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.WriteFile("openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: /etc/passwd#/User
+      responses:
+        "200":
+          description: ok
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi external ref must be relative")
+}
+
+func TestApply_RejectsRemoteExternalRef(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.WriteFile("openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: https://example.com/schemas/user.yaml#/User
+      responses:
+        "200":
+          description: ok
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi external ref must be relative")
+}
+
+func TestApply_RejectsNestedExternalRefEscapingAllowedDirectoryViaSymlink(t 
*testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.MkdirAll("specs/schemas", 0o700))
+       require.NoError(t, os.Symlink("/etc", filepath.Join(dir, "specs", 
"schemas", "current")))
+       require.NoError(t, os.WriteFile("specs/openapi.yaml", []byte(`
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: ./schemas/user.yaml#/User
+      responses:
+        "200":
+          description: ok
+`), 0o600))
+       require.NoError(t, os.WriteFile("specs/schemas/user.yaml", []byte(`
+User:
+  type: object
+  properties:
+    name:
+      $ref: ./current/passwd#/Name
+`), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path: "specs/openapi.yaml",
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "openapi external ref escapes allowed 
directory")
+}
+
+func TestApply_RejectsNegativeMaxRequestBodyBytes(t *testing.T) {
+       dir := t.TempDir()
+       t.Chdir(dir)
+       require.NoError(t, os.WriteFile("openapi.yaml", 
[]byte(usersSpecWithQueryAndBody()), 0o600))
+
+       factory := &FilterFactory{
+               cfg: &Config{
+                       Path:                "openapi.yaml",
+                       MaxRequestBodyBytes: -1,
+               },
+       }
+
+       err := factory.Apply()
+
+       require.Error(t, err)
+       assert.Contains(t, err.Error(), "max_request_body_bytes must not be 
negative")
+}
+
+func TestApply_UsesDefaultMaxRequestBodyBytesForZeroValue(t *testing.T) {
+       factory := newOpenAPIFactory(t, usersSpecWithQueryAndBody(), func(cfg 
*Config) {
+               cfg.MaxRequestBodyBytes = 0
+       })
+
+       assert.Equal(t, int64(defaultMaxRequestBodyBytes), 
factory.maxRequestBody)
+}
+
+func TestApply_BuildsReusablePathLookupOptions(t *testing.T) {
+       factory := newOpenAPIFactory(t, usersSpecWithQueryAndBody(), nil)
+
+       require.NotNil(t, factory.validationOptions)
+       require.NotNil(t, factory.validationOptions.PathTree)
+
+       filterInstance := &Filter{
+               validator:         factory.validator,
+               model:             factory.model,
+               validationOptions: factory.validationOptions,
+       }
+       req := httptest.NewRequest(http.MethodPost, "/users?source=web", 
strings.NewReader(`{"name":"tom"}`))
+
+       pathItem, foundPath, ok := filterInstance.findRequestOperation(req)
+
+       require.True(t, ok)
+       require.NotNil(t, pathItem)
+       assert.Equal(t, "/users", foundPath)
+}
+
+func TestDecode_ValidatesTemplatedPaths(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, `
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users/{id}:
+    get:
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema:
+            type: integer
+      responses:
+        "200":
+          description: ok
+`)
+       req := httptest.NewRequest(http.MethodGet, "/users/abc", nil)
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusBadRequest, recorder.Code)
+       assert.True(t, ctx.LocalReply())
+}
+
+func TestDecode_ValidatesHeaderRequiredAndType(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, `
+openapi: 3.0.3
+info:
+  title: reports
+  version: "1.0.0"
+paths:
+  /reports:
+    get:
+      parameters:
+        - name: X-Tenant-ID
+          in: header
+          required: true
+          schema:
+            type: integer
+      responses:
+        "200":
+          description: ok
+`)
+
+       tests := []struct {
+               name        string
+               headerValue string
+               wantStatus  extfilter.FilterStatus
+       }{
+               {
+                       name:       "missing required header",
+                       wantStatus: extfilter.Stop,
+               },
+               {
+                       name:        "invalid header type",
+                       headerValue: "tenant-a",
+                       wantStatus:  extfilter.Stop,
+               },
+               {
+                       name:        "valid header",
+                       headerValue: "42",
+                       wantStatus:  extfilter.Continue,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       req := httptest.NewRequest(http.MethodGet, "/reports", 
nil)
+                       if tt.headerValue != "" {
+                               req.Header.Set("X-Tenant-ID", tt.headerValue)
+                       }
+                       recorder := httptest.NewRecorder()
+                       ctx := &contexthttp.HttpContext{Request: req, Writer: 
recorder}
+
+                       status := filterInstance.Decode(ctx)
+
+                       assert.Equal(t, tt.wantStatus, status)
+                       assert.Equal(t, tt.wantStatus == extfilter.Stop, 
ctx.LocalReply())
+               })
+       }
+}
+
+func TestDecode_ValidatesQueryEnum(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+       req := httptest.NewRequest(http.MethodPost, "/users?source=cli", 
strings.NewReader(`{"name":"tom"}`))
+       req.Header.Set("Content-Type", "application/json")
+       recorder := httptest.NewRecorder()
+       ctx := &contexthttp.HttpContext{Request: req, Writer: recorder}
+
+       status := filterInstance.Decode(ctx)
+
+       assert.Equal(t, extfilter.Stop, status)
+       assert.Equal(t, http.StatusBadRequest, recorder.Code)
+       assert.True(t, ctx.LocalReply())
+}
+
+func TestDecode_ValidatesRequestBodyMinAndMaxLength(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+
+       tests := []struct {
+               name       string
+               body       string
+               wantStatus extfilter.FilterStatus
+       }{
+               {
+                       name:       "below minLength",
+                       body:       `{"name":"Al"}`,
+                       wantStatus: extfilter.Stop,
+               },
+               {
+                       name:       "above maxLength",
+                       body:       `{"name":"Alexandria"}`,
+                       wantStatus: extfilter.Stop,
+               },
+               {
+                       name:       "within length bounds",
+                       body:       `{"name":"Alice"}`,
+                       wantStatus: extfilter.Continue,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       req := httptest.NewRequest(http.MethodPost, 
"/users?source=web", strings.NewReader(tt.body))
+                       req.Header.Set("Content-Type", "application/json")
+                       recorder := httptest.NewRecorder()
+                       ctx := &contexthttp.HttpContext{Request: req, Writer: 
recorder}
+
+                       status := filterInstance.Decode(ctx)
+
+                       assert.Equal(t, tt.wantStatus, status)
+                       assert.Equal(t, tt.wantStatus == extfilter.Stop, 
ctx.LocalReply())
+               })
+       }
+}
+
+func TestDecode_ValidatesRequestBodyMinimumAndMaximum(t *testing.T) {
+       filterInstance := newOpenAPIFilter(t, usersSpecWithQueryAndBody())
+
+       tests := []struct {
+               name       string
+               body       string
+               wantStatus extfilter.FilterStatus
+       }{
+               {
+                       name:       "below minimum",
+                       body:       `{"name":"Alice","age":12}`,
+                       wantStatus: extfilter.Stop,
+               },
+               {
+                       name:       "above maximum",
+                       body:       `{"name":"Alice","age":121}`,
+                       wantStatus: extfilter.Stop,
+               },
+               {
+                       name:       "within numeric bounds",
+                       body:       `{"name":"Alice","age":18}`,
+                       wantStatus: extfilter.Continue,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       req := httptest.NewRequest(http.MethodPost, 
"/users?source=web", strings.NewReader(tt.body))
+                       req.Header.Set("Content-Type", "application/json")
+                       recorder := httptest.NewRecorder()
+                       ctx := &contexthttp.HttpContext{Request: req, Writer: 
recorder}
+
+                       status := filterInstance.Decode(ctx)
+
+                       assert.Equal(t, tt.wantStatus, status)
+                       assert.Equal(t, tt.wantStatus == extfilter.Stop, 
ctx.LocalReply())
+               })
+       }
+}
+
+func newOpenAPIFilter(t *testing.T, spec string) *Filter {
+       t.Helper()
+       factory := newOpenAPIFactory(t, spec, nil)
+       return &Filter{
+               validator:         factory.validator,
+               model:             factory.model,
+               validationOptions: factory.validationOptions,
+               maxRequestBody:    factory.maxRequestBody,
+       }
+}
+
+func newOpenAPIFilterWithConfig(t *testing.T, spec string, configure 
func(*Config)) *Filter {
+       t.Helper()
+       factory := newOpenAPIFactory(t, spec, configure)
+       return &Filter{
+               validator:         factory.validator,
+               model:             factory.model,
+               validationOptions: factory.validationOptions,
+               maxRequestBody:    factory.maxRequestBody,
+       }
+}
+
+func newOpenAPIFactory(t *testing.T, spec string, configure func(*Config)) 
*FilterFactory {
+       t.Helper()
+
+       dir := t.TempDir()
+       t.Chdir(dir)
+       specPath := "openapi.yaml"
+       require.NoError(t, os.WriteFile(filepath.Join(dir, specPath), 
[]byte(spec), 0o600))
+
+       cfg := &Config{
+               Path: specPath,
+       }
+       if configure != nil {
+               configure(cfg)
+       }
+
+       factory := &FilterFactory{
+               cfg: cfg,
+       }
+       require.NoError(t, factory.Apply())
+
+       return factory
+}
+
+func usersSpecWithQueryAndBody() string {
+       return `
+openapi: 3.0.3
+info:
+  title: users
+  version: "1.0.0"
+paths:
+  /users:
+    post:
+      parameters:
+        - name: source
+          in: query
+          required: true
+          schema:
+            type: string
+            enum: [web, app]
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required: [name]
+              properties:
+                name:
+                  type: string
+                  minLength: 3
+                  maxLength: 8
+                age:
+                  type: integer
+                  minimum: 13
+                  maximum: 120
+      responses:
+        "200":
+          description: ok
+`
+}
diff --git a/pkg/pluginregistry/registry.go b/pkg/pluginregistry/registry.go
index ead498471..cc7c10acf 100644
--- a/pkg/pluginregistry/registry.go
+++ b/pkg/pluginregistry/registry.go
@@ -46,6 +46,7 @@ import (
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/grpcproxy"
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/httpproxy"
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/loadbalancer"
+       _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/openapi"
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/proxyrewrite"
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/http/remote"
        _ "github.com/apache/dubbo-go-pixiu/pkg/filter/llm/proxy"

Reply via email to