This is an automated email from the ASF dual-hosted git repository.
bzp2010 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix.git
The following commit(s) were added to refs/heads/master by this push:
new 97b612204a feat(websocket): add plugin to customize proxy behaviors
(#13972)
97b612204a is described below
commit 97b612204acbb68c0143f89d077d5a459b65ba00
Author: Zeping Bai <[email protected]>
AuthorDate: Mon Sep 21 14:45:52 2026 +0800
feat(websocket): add plugin to customize proxy behaviors (#13972)
---
apisix-master-0.rockspec | 2 +-
apisix/cli/config.lua | 1 +
apisix/init.lua | 18 ++++
apisix/plugins/websocket-proxy.lua | 73 +++++++++++++++
docs/en/latest/config.json | 1 +
docs/en/latest/plugins/websocket-proxy.md | 114 +++++++++++++++++++++++
docs/zh/latest/config.json | 1 +
docs/zh/latest/plugins/websocket-proxy.md | 89 ++++++++++++++++++
t/admin/plugins.t | 1 +
t/lib/server.lua | 128 ++++++++++++++++++++++++++
t/node/websocket-proxy.spec.mts | 148 ++++++++++++++++++++++++++++++
11 files changed, 575 insertions(+), 1 deletion(-)
diff --git a/apisix-master-0.rockspec b/apisix-master-0.rockspec
index 8c1fc783bd..5a0137bc05 100644
--- a/apisix-master-0.rockspec
+++ b/apisix-master-0.rockspec
@@ -32,7 +32,7 @@ description = {
dependencies = {
"lua-resty-ctxdump = 0.1-0",
- "api7-lua-resty-websocket = 0.1.0-0",
+ "api7-lua-resty-websocket = 0.2.0-0",
"api7-lua-resty-redis-connector = 0.13.0",
"lyaml = 6.2.8-1",
"api7-lua-resty-dns-client = 7.1.2-0",
diff --git a/apisix/cli/config.lua b/apisix/cli/config.lua
index 464f839eee..ab034eeea8 100644
--- a/apisix/cli/config.lua
+++ b/apisix/cli/config.lua
@@ -276,6 +276,7 @@ local _M = {
"mcp-bridge",
"degraphql",
"kafka-proxy",
+ "websocket-proxy",
"grpc-transcode",
"grpc-web",
"http-dubbo",
diff --git a/apisix/init.lua b/apisix/init.lua
index 4e78d2335a..9440ea9e13 100644
--- a/apisix/init.lua
+++ b/apisix/init.lua
@@ -1110,9 +1110,27 @@ function _M.websocket_content_phase()
end
end
+ -- max_recv_len/max_send_len default to 65535 on each side unless the
+ -- websocket-proxy plugin's ws_handshake set larger limits on ctx. Each
+ -- side's receive limit is its own configured value; its send limit is
+ -- the *other* side's configured value, since what a role sends out is
+ -- always a message it just relayed in from its counterpart (a message
+ -- the real client sent needs the upstream side's send limit raised to
+ -- match the client side's receive limit, and vice versa). A nil field
+ -- keeps that direction's library default instead of silently raising it.
+ local client_new_opts, upstream_new_opts
+ local client_max_len = api_ctx.websocket_proxy_client_max_payload_len
+ local upstream_max_len = api_ctx.websocket_proxy_upstream_max_payload_len
+ if client_max_len or upstream_max_len then
+ client_new_opts = {max_recv_len = client_max_len, max_send_len =
upstream_max_len}
+ upstream_new_opts = {max_recv_len = upstream_max_len, max_send_len =
client_max_len}
+ end
+
local ok, proxy, err = pcall(ws_proxy.new, {
aggregate_fragments = true,
recv_timeout = recv_timeout_ms,
+ client_new_opts = client_new_opts,
+ upstream_new_opts = upstream_new_opts,
on_frame = function(proxy, role, typ, payload, last, code)
-- proxy: [table] the proxy instance
-- role: [string] "client" or "upstream"
diff --git a/apisix/plugins/websocket-proxy.lua
b/apisix/plugins/websocket-proxy.lua
new file mode 100644
index 0000000000..4946776f1f
--- /dev/null
+++ b/apisix/plugins/websocket-proxy.lua
@@ -0,0 +1,73 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements. See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License. You may obtain a copy of the License at
+--
+-- http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local core = require("apisix.core")
+
+
+-- "client"/"upstream" here name the same two roles resty.websocket.proxy
+-- itself uses: "client" is the side facing the real downstream WebSocket
+-- client, "upstream" is the side facing the backend. Left unset, the
+-- library defaults max_payload_len (and, through it, the max size of a
+-- single unfragmented frame) to 65535 on both sides.
+--
+-- 2147483647 (0x7fffffff) is api7-lua-resty-websocket's own hard ceiling:
+-- protocol.lua's send_frame() only encodes a 31-bit length and refuses to
+-- send anything past it ("payload too big"), so a configured value beyond
+-- this bound would pass schema validation but could never actually be
+-- honored end to end.
+local MAX_PAYLOAD_LEN_CEILING = 2147483647
+
+local schema = {
+ type = "object",
+ properties = {
+ client_max_payload_len = {
+ type = "integer",
+ minimum = 1,
+ maximum = MAX_PAYLOAD_LEN_CEILING,
+ description = "max size, in bytes, of a single WebSocket " ..
+ "message this route will accept from the downstream client",
+ },
+ upstream_max_payload_len = {
+ type = "integer",
+ minimum = 1,
+ maximum = MAX_PAYLOAD_LEN_CEILING,
+ description = "max size, in bytes, of a single WebSocket " ..
+ "message this route will accept from the upstream",
+ },
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 511,
+ name = "websocket-proxy",
+ schema = schema,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+function _M.ws_handshake(conf, ctx)
+ ctx.websocket_proxy_client_max_payload_len = conf.client_max_payload_len
+ ctx.websocket_proxy_upstream_max_payload_len =
conf.upstream_max_payload_len
+end
+
+
+return _M
diff --git a/docs/en/latest/config.json b/docs/en/latest/config.json
index c2a816aecf..7397a7cf7a 100644
--- a/docs/en/latest/config.json
+++ b/docs/en/latest/config.json
@@ -254,6 +254,7 @@
"plugins/dubbo-proxy",
"plugins/mqtt-proxy",
"plugins/kafka-proxy",
+ "plugins/websocket-proxy",
"plugins/http-dubbo",
"plugins/mcp-bridge",
"plugins/openapi-to-mcp"
diff --git a/docs/en/latest/plugins/websocket-proxy.md
b/docs/en/latest/plugins/websocket-proxy.md
new file mode 100644
index 0000000000..0fe2e7cdcf
--- /dev/null
+++ b/docs/en/latest/plugins/websocket-proxy.md
@@ -0,0 +1,114 @@
+---
+title: websocket-proxy
+keywords:
+ - Apache APISIX
+ - API Gateway
+ - Plugin
+ - WebSocket proxy
+description: This document contains information about the Apache APISIX
websocket-proxy Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Description
+
+The `websocket-proxy` plugin configures advanced parameters for a route whose
`upstream.scheme` is
+`ws` or `wss`. It currently controls the maximum size of a single WebSocket
frame APISIX accepts on
+each side of the connection.
+
+By default, APISIX accepts a single frame of up to 65535 bytes from either the
downstream client or
+the upstream; a larger single frame closes the connection. `enable_websocket`
has no such limit,
+since it lets nginx relay raw bytes without parsing frames, but `scheme:
ws`/`wss` parses every
+frame in order to run plugin logic against it, and the underlying library caps
a single frame's size
+unless told otherwise. This plugin raises that cap for routes that need to
send or receive larger
+messages, such as a client uploading a file in one WebSocket message.
+
+## Attributes
+
+Each attribute below is an endpoint-level limit, not just a "receive from that
peer" limit: it also
+raises the send limit on the *other* endpoint, since a message relayed onward
is always sent back
+out through the opposite side of the proxy. Setting only
`client_max_payload_len` is therefore
+enough to let a large client message all the way through to the upstream: it
raises both how much
+the client-facing side accepts and how much the upstream-facing side is
allowed to send. The two
+attributes are independent of each other, so an asymmetric configuration (one
raised, the other left
+at the default, or both raised to different values) is valid and does the
expected thing in each
+direction.
+
+| Name | Type | Required | Default | Valid values
| Description |
+|---------------------------|---------|----------|---------|------------------------|-------------|
+| client_max_payload_len | integer | optional | | 1 - 2147483647
| Max size, in bytes, of a single WebSocket message this route accepts
from the downstream client, and the max size it will relay from the client out
to the upstream. Left unset, the default of 65535 applies. |
+| upstream_max_payload_len | integer | optional | | 1 - 2147483647
| Max size, in bytes, of a single WebSocket message this route accepts
from the upstream, and the max size it will relay from the upstream out to the
client. Left unset, the default of 65535 applies. |
+
+## Example usage
+
+Create a route with `upstream.scheme` set to `ws`, and raise the frame size
limit on both sides with
+this plugin:
+
+```shell
+curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \
+ -H 'X-API-KEY: <api-key>' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "uri": "/ws",
+ "plugins": {
+ "websocket-proxy": {
+ "client_max_payload_len": 1048576,
+ "upstream_max_payload_len": 1048576
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin",
+ "scheme": "ws"
+ }
+}'
+```
+
+Now, a WebSocket message of up to 1 MiB in either direction on `/ws` no longer
closes the connection.
+
+## FAQ
+
+### Does this plugin apply to a route using `enable_websocket`?
+
+No. It only takes effect on a route whose `upstream.scheme` is `ws` or `wss`.
`enable_websocket`
+uses nginx's own `proxy_pass` to relay raw bytes without parsing frames at
all, so there is no
+frame-size limit for this plugin to raise there in the first place. See the
+[scheme description in the Admin API reference](../admin-api.md#upstream) for
the difference
+between the two.
+
+### I raised `client_max_payload_len`, but a large message from the upstream
still does not reach the client
+
+The two attributes are independent of each other. `client_max_payload_len`
covers a
+client-originated message in both directions (see [Attributes](#attributes));
a large
+upstream-originated message needs `upstream_max_payload_len` raised instead.
+
+### My configuration was rejected with a schema error mentioning 2147483647
+
+`api7-lua-resty-websocket` only encodes a 31-bit frame length, so both
attributes reject a value
+above 2147483647 (2^31 - 1) at configuration time, since the library could
never actually honor it.
+Lower the value, or split the payload into multiple WebSocket messages, if you
need more than that.
+
+## Delete Plugin
+
+To remove the `websocket-proxy` Plugin, you can delete the corresponding JSON
configuration from the
+Plugin configuration. APISIX will automatically reload and you do not have to
restart for this to
+take effect.
diff --git a/docs/zh/latest/config.json b/docs/zh/latest/config.json
index 4217ba5552..baf9fb0b1b 100644
--- a/docs/zh/latest/config.json
+++ b/docs/zh/latest/config.json
@@ -239,6 +239,7 @@
"items": [
"plugins/dubbo-proxy",
"plugins/mqtt-proxy",
+ "plugins/websocket-proxy",
"plugins/http-dubbo",
"plugins/openapi-to-mcp"
]
diff --git a/docs/zh/latest/plugins/websocket-proxy.md
b/docs/zh/latest/plugins/websocket-proxy.md
new file mode 100644
index 0000000000..90cb54f2a8
--- /dev/null
+++ b/docs/zh/latest/plugins/websocket-proxy.md
@@ -0,0 +1,89 @@
+---
+title: websocket-proxy
+keywords:
+ - Apache APISIX
+ - API 网关
+ - Plugin
+ - WebSocket proxy
+description: 本文介绍了关于 Apache APISIX `websocket-proxy` 插件的基本信息及使用方法。
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## 描述
+
+`websocket-proxy` 插件用于给 `upstream.scheme` 为 `ws` 或 `wss` 的路由配置高级参数。目前它控制的是
APISIX 在连接两侧各自能接受的单个 WebSocket 帧的最大尺寸。
+
+默认情况下,APISIX 接受来自下游客户端或上游的单帧最大为 65535 字节,超过这个尺寸的单帧会导致连接被关闭。`enable_websocket`
没有这个限制,因为它让 nginx 直接转发原始字节、不解析帧;但 `scheme: ws`/`wss`
会解析每一帧以便运行插件逻辑,底层库如果不特别设置就会对单帧尺寸设上限。这个插件就是给需要收发更大消息的路由(比如客户端用一个 WebSocket
消息上传一个文件)放开这个上限用的。
+
+## 属性
+
+下表里每一项都是"端点级"的限制,不只是"从对端接收"的限制:它还会同时抬高*另一个*端点的发送上限,因为转发出去的消息,本质上都是从对端刚收进来又原样发出去的。所以只配置
`client_max_payload_len`
就足够让一条大的客户端消息一路转发到上游:它同时抬高了面向客户端一侧能接受多大的消息、以及面向上游一侧被允许发送多大的消息。这两个属性彼此独立,所以不对称的配置(只抬高一个、另一个保持默认,或者两个配成不同的值)是合法的,各自方向都会按预期工作。
+
+| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
+|---------------------------|---------|-----|--------|-----------------|------|
+| client_max_payload_len | integer | 否 | | 1 - 2147483647 |
这个路由能接受的、来自下游客户端的单个 WebSocket 消息的最大字节数,同时也是它能从客户端转发到上游的单个消息的最大字节数。不设置时按默认值
65535 处理。 |
+| upstream_max_payload_len | integer | 否 | | 1 - 2147483647 |
这个路由能接受的、来自上游的单个 WebSocket 消息的最大字节数,同时也是它能从上游转发到客户端的单个消息的最大字节数。不设置时按默认值 65535
处理。 |
+
+## 示例
+
+创建一个 `upstream.scheme` 为 `ws` 的路由,并用这个插件把两侧的帧尺寸上限都放开:
+
+```shell
+curl -X PUT 'http://127.0.0.1:9180/apisix/admin/routes/r1' \
+ -H 'X-API-KEY: <api-key>' \
+ -H 'Content-Type: application/json' \
+ -d '{
+ "uri": "/ws",
+ "plugins": {
+ "websocket-proxy": {
+ "client_max_payload_len": 1048576,
+ "upstream_max_payload_len": 1048576
+ }
+ },
+ "upstream": {
+ "nodes": {
+ "127.0.0.1:1980": 1
+ },
+ "type": "roundrobin",
+ "scheme": "ws"
+ }
+}'
+```
+
+现在,`/ws` 这条路由上不管哪个方向、单条消息只要不超过 1 MiB 就不会再导致连接被关闭。
+
+## FAQ
+
+### 这个插件对使用 `enable_websocket` 的路由生效吗?
+
+不生效。它只对 `upstream.scheme` 为 `ws` 或 `wss` 的路由起作用。`enable_websocket` 用的是 nginx
自己的 `proxy_pass` 直接转发原始字节,压根不解析帧,所以在那条路径上也就没有这个插件要放开的帧尺寸限制可言。两者的区别可以参考 [Admin
API 参考文档里 scheme 的说明](../admin-api.md#upstream)。
+
+### 我抬高了 `client_max_payload_len`,但上游发来的大消息还是到不了客户端
+
+这两个属性彼此独立。`client_max_payload_len`
覆盖的是客户端发起的消息在两个方向上的转发(参见[属性](#属性));上游发起的大消息需要抬高的是 `upstream_max_payload_len`。
+
+### 我的配置被拒绝了,报错里提到 2147483647
+
+`api7-lua-resty-websocket` 只用 31 位编码帧长度,所以这两个属性都不接受超过 2147483647(2^31 -
1)的值,超过这个值在配置阶段就会被拒绝,而不是留到运行时才不可预知地失败。如果需要更大的消息,把这个值调小,或者把要传的内容拆成多个 WebSocket
消息发送。
+
+## 删除插件
+
+要移除 `websocket-proxy` 插件,只需在插件配置中删除相应的 JSON 配置,APISIX 会自动重新加载,无需重启服务。
diff --git a/t/admin/plugins.t b/t/admin/plugins.t
index 5bd48edbeb..b15b64d696 100644
--- a/t/admin/plugins.t
+++ b/t/admin/plugins.t
@@ -129,6 +129,7 @@ redirect
response-rewrite
openapi-to-mcp
oas-validator
+websocket-proxy
mcp-bridge
degraphql
kafka-proxy
diff --git a/t/lib/server.lua b/t/lib/server.lua
index 757c3c584a..ab0782d7d1 100644
--- a/t/lib/server.lua
+++ b/t/lib/server.lua
@@ -430,6 +430,134 @@ function _M.websocket_echo()
end
+-- Like websocket_echo, but with a raised max_payload_len (and, through it,
+-- max_recv_len/max_send_len) so this fixture itself is never the bottleneck
+-- for a >64K single-frame test: whatever the test observes then comes from
+-- the proxy sitting in front of it, not from this fixture's own default.
+function _M.websocket_echo_large()
+ local websocket = require "resty.websocket.server"
+ local wb, err = websocket:new({max_payload_len = 4 * 1024 * 1024})
+ if not wb then
+ ngx.log(ngx.ERR, "failed to new websocket: ", err)
+ return ngx.exit(400)
+ end
+
+ while true do
+ local data, typ, err = wb:recv_frame()
+ if not data then
+ if err and err:find("timeout", 1, true) then
+ goto continue
+ end
+ ngx.log(ngx.ERR, "failed to receive frame: ", err)
+ return
+ end
+
+ if typ == "close" then
+ wb:send_close(1000, "")
+ return
+ elseif typ == "ping" then
+ wb:send_pong(data)
+ elseif typ == "text" or typ == "binary" then
+ local send = typ == "text" and wb.send_text or wb.send_binary
+ local bytes, send_err = send(wb, data)
+ if not bytes then
+ ngx.log(ngx.ERR, "failed to echo frame: ", send_err)
+ return
+ end
+ end
+
+ ::continue::
+ end
+end
+
+
+-- Like websocket_echo_large, but replies to every text/binary frame with a
+-- short "received:<n>" ack instead of echoing the payload back, so a test
+-- can send a large frame in and only needs a large *receive* limit on the
+-- proxy in front of it, not also a large *send* limit for the reply.
+function _M.websocket_ack_large()
+ local websocket = require "resty.websocket.server"
+ local wb, err = websocket:new({max_payload_len = 4 * 1024 * 1024})
+ if not wb then
+ ngx.log(ngx.ERR, "failed to new websocket: ", err)
+ return ngx.exit(400)
+ end
+
+ while true do
+ local data, typ, err = wb:recv_frame()
+ if not data then
+ if err and err:find("timeout", 1, true) then
+ goto continue
+ end
+ ngx.log(ngx.ERR, "failed to receive frame: ", err)
+ return
+ end
+
+ if typ == "close" then
+ wb:send_close(1000, "")
+ return
+ elseif typ == "ping" then
+ wb:send_pong(data)
+ elseif typ == "text" or typ == "binary" then
+ local bytes, send_err = wb:send_text("received:" .. #data)
+ if not bytes then
+ ngx.log(ngx.ERR, "failed to send ack: ", send_err)
+ return
+ end
+ end
+
+ ::continue::
+ end
+end
+
+
+-- Like websocket_echo_large, but pushes one large ("x" * 1MiB) text frame
+-- right after the handshake, unprompted, so a test can observe a large
+-- upstream-to-client message without also having to send a large one itself.
+-- Falls into the same echo loop afterwards.
+function _M.websocket_send_large()
+ local websocket = require "resty.websocket.server"
+ local wb, err = websocket:new({max_payload_len = 4 * 1024 * 1024})
+ if not wb then
+ ngx.log(ngx.ERR, "failed to new websocket: ", err)
+ return ngx.exit(400)
+ end
+
+ local bytes, send_err = wb:send_text(string.rep("x", 1024 * 1024))
+ if not bytes then
+ ngx.log(ngx.ERR, "failed to send large frame: ", send_err)
+ return
+ end
+
+ while true do
+ local data, typ, err = wb:recv_frame()
+ if not data then
+ if err and err:find("timeout", 1, true) then
+ goto continue
+ end
+ ngx.log(ngx.ERR, "failed to receive frame: ", err)
+ return
+ end
+
+ if typ == "close" then
+ wb:send_close(1000, "")
+ return
+ elseif typ == "ping" then
+ wb:send_pong(data)
+ elseif typ == "text" or typ == "binary" then
+ local send = typ == "text" and wb.send_text or wb.send_binary
+ local ok, echo_err = send(wb, data)
+ if not ok then
+ ngx.log(ngx.ERR, "failed to echo frame: ", echo_err)
+ return
+ end
+ end
+
+ ::continue::
+ end
+end
+
+
-- Like websocket_echo, but the first thing it sends back is a text frame
-- carrying the request URI (with query string) it was actually dispatched
-- with, so a test can confirm what path/query a fronting proxy forwarded.
diff --git a/t/node/websocket-proxy.spec.mts b/t/node/websocket-proxy.spec.mts
index 7b85d68699..a1043b447e 100644
--- a/t/node/websocket-proxy.spec.mts
+++ b/t/node/websocket-proxy.spec.mts
@@ -451,6 +451,154 @@ describe('websocket-proxy (ws/wss upstream scheme)', ()
=> {
});
});
+ describe('frame size (websocket-proxy plugin)', () => {
+ it('closes the connection on a single frame over the 65535-byte default',
async () => {
+ await putEchoRoute({
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ });
+
+ const code = await waitForClose(
+ '/websocket_echo',
+ (ws) => ws.send('x'.repeat(70000)),
+ true,
+ );
+ expect(code).toBe(1006);
+ }, 10000);
+
+ it('forwards a >64K frame in each direction once websocket-proxy raises
max_payload_len', async () => {
+ await createRoute(
+ '/websocket_echo_large',
+ {
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ },
+ {
+ 'websocket-proxy': {
+ client_max_payload_len: 2 * 1024 * 1024,
+ upstream_max_payload_len: 2 * 1024 * 1024,
+ },
+ },
+ );
+
+ const payload = 'x'.repeat(1024 * 1024);
+ const reply = await sendAndReceive('/websocket_echo_large', payload);
+ expect(reply).toBe(payload);
+ }, 15000);
+
+ it('relays a >64K client message once only client_max_payload_len is
raised, with no reply raised', async () => {
+ // websocket_ack_large replies with a short "received:<n>" ack instead
+ // of echoing, so this only exercises the client-to-upstream direction:
+ // it would still pass even if the (unconfigured) reply-out-to-client
+ // send limit were wrongly stuck at the default.
+ await createRoute(
+ '/websocket_ack_large',
+ {
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ },
+ {
+ 'websocket-proxy': { client_max_payload_len: 2 * 1024 * 1024 },
+ },
+ );
+
+ const payload = 'x'.repeat(200000);
+ const reply = await sendAndReceive('/websocket_ack_large', payload);
+ expect(reply).toBe(`received:${payload.length}`);
+ }, 15000);
+
+ it('relays a >64K upstream push once only upstream_max_payload_len is
raised, with no client message sent', async () => {
+ // websocket_send_large pushes a 1MiB frame unprompted right after the
+ // handshake; only the upstream-to-client direction needs raising here,
+ // so this catches a fix that raised the client's own receive limit
+ // (irrelevant, nothing large is sent to it) instead of its send limit.
+ await createRoute(
+ '/websocket_send_large',
+ {
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ },
+ {
+ 'websocket-proxy': { upstream_max_payload_len: 2 * 1024 * 1024 },
+ },
+ );
+
+ const reply = await new Promise<string>((resolve, reject) => {
+ const ws = new WebSocket(`${PROXY_BASE}/websocket_send_large`);
+ ws.addEventListener('message', (ev) => {
+ resolve(ev.data as string);
+ ws.close();
+ });
+ ws.addEventListener('error', (ev) =>
+ reject(new Error((ev as unknown as { message?: string }).message ??
'websocket error')),
+ );
+ });
+ expect(reply).toBe('x'.repeat(1024 * 1024));
+ }, 15000);
+
+ it('lets the larger of two asymmetric limits govern its own direction, not
the smaller one', async () => {
+ // client_max_payload_len (100000) is smaller than the 1MiB push below,
+ // upstream_max_payload_len (2MiB) is larger: the push must still get
+ // through on the strength of the upstream-side limit alone, proving
+ // the two directions are not tied to the same configured value.
+ await createRoute(
+ '/websocket_send_large_asymmetric',
+ {
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ },
+ {
+ 'websocket-proxy': {
+ client_max_payload_len: 100000,
+ upstream_max_payload_len: 2 * 1024 * 1024,
+ },
+ // the fixture is dispatched by URI (see t/lib/server.lua's go()),
+ // so rewrite this route's distinct client-facing path back to the
+ // websocket_send_large fixture the previous test already uses
+ 'proxy-rewrite': { uri: '/websocket_send_large' },
+ },
+ );
+
+ const reply = await new Promise<string>((resolve, reject) => {
+ const ws = new
WebSocket(`${PROXY_BASE}/websocket_send_large_asymmetric`);
+ ws.addEventListener('message', (ev) => {
+ resolve(ev.data as string);
+ ws.close();
+ });
+ ws.addEventListener('error', (ev) =>
+ reject(new Error((ev as unknown as { message?: string }).message ??
'websocket error')),
+ );
+ });
+ expect(reply).toBe('x'.repeat(1024 * 1024));
+ }, 15000);
+
+ it('rejects a payload len beyond the library\'s 2147483647 (2^31 - 1)
frame length limit', async () => {
+ const res = await requestAdminAPI(
+ '/apisix/admin/routes/ws-proxy-oversized-limit',
+ 'PUT',
+ {
+ uri: '/websocket_echo_large',
+ upstream: {
+ type: 'roundrobin',
+ scheme: 'ws',
+ nodes: { [ECHO_NODE]: 1 },
+ },
+ plugins: {
+ 'websocket-proxy': { client_max_payload_len: 2147483648 },
+ },
+ },
+ undefined,
+ { validateStatus: () => true },
+ );
+ expect(res.status).toBe(400);
+ });
+ });
+
describe('concurrent connections', () => {
it("keeps two simultaneous connections' frame data isolated from each
other", async () => {
await putEchoRoute(