Configure WebSocket Proxying
Proxy a WebSocket connection through an API7 Gateway route and verify the HTTP Upgrade handshake.
Applies to:API7 Gateway 3.10.x
Roles:Platform engineer
Goal
A route that completes a WebSocket Upgrade handshake and proxies the resulting connection to an echo server.
Before you begin
Set the variables before running any checks:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default" # only the group the quickstart creates; otherwise copy the gateway group's UUID from Gateway Groups in the Dashboard
read -rp "Gateway group name for ADC (if used): " API7_GATEWAY_GROUP_NAME && export API7_GATEWAY_GROUP_NAME
export GATEWAY_URL="http://127.0.0.1:9080"
read -rsp "Dashboard token: " API7_ADMIN_KEY && export API7_ADMIN_KEY && echo-
An API7 Gateway control plane is reachable and your Dashboard token is valid:
curl -ks -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" \ -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected:
200. -
A gateway instance in the selected group reports
Healthy:curl -ks "${API7_ADMIN_URL}/api/gateway_groups/${API7_GATEWAY_GROUP_ID}/instances" \ -H "X-API-KEY: ${API7_ADMIN_KEY}" | grep -o '"status": *"Healthy"' | wc -lExpected:
1or more. -
Docker and
curlare available:docker version --format '{{.Client.Version}}' curl --version | head -1Expected: a Docker client version and a
curlversion line. -
No container named
websocket-serveralready exists, and no running Docker container already publishes local port8080:if docker container inspect websocket-server >/dev/null 2>&1; then echo 'Stop: container websocket-server already exists'; exit 1; fi if docker container ls --format '{{.Ports}}' | grep -Eq '(^|, |:)8080->'; then echo 'Stop: a Docker container already publishes local port 8080'; exit 1; fiExpected: no output. If either check stops, use different example names and ports throughout or remove only the conflicting resources you own.
-
For the ADC path, ADC is installed and supports label selectors:
adc --version adc sync --help | grep -- '--label-selector'Expected: a version string and a help entry for
--label-selector. The option is documented in ADCv0.30.3; use a version whose help lists it. ADC looks up--gateway-groupby gateway group display name, soAPI7_GATEWAY_GROUP_NAMEis distinct from the UUID used by the compatibility Admin API. Enter the name of the same group identified byAPI7_GATEWAY_GROUP_ID. If similar group names make the lookup ambiguous, stop and confirm which group your installed ADC resolves before continuing.ADC writes
diff.yamlin the current directory. Before saving or running this guide's ADC examples, confirm that none of its local paths already exists:for file in websocket-adc.yaml websocket-cleanup.yaml diff.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; doneExpected: no output. This makes the local cleanup safe; otherwise use a new empty working directory.
This guide creates only websocket-service and websocket-route. Check that both IDs are unused:
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/websocket-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -w '\nHTTP status: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/routes/websocket-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"For the Admin API path, expected: each response reports that the requested resource is absent. If a response has HTTP status 200 and a populated matching value.id, that ID is already in use; stop or change every occurrence before continuing. Do not treat another error as proof of absence: correct the token, gateway group, or request until the result is unambiguous. The PUT operations below replace a matching ID.
ADC can encounter an object with the same name under a different backend ID, so the ID check alone does not prove ADC ownership. On the ADC path, continue only if adc diff shows creates for exactly the service and its nested route, with no update, replacement, or deletion of an existing object.
The examples use host.docker.internal, which Docker Desktop resolves to the host. If the gateway does not run in Docker Desktop, use an address that the gateway can reach in both configuration paths.
Steps
Start the echo server, then choose either the Admin API path in steps 2–3 or the ADC path in step 4. Do not apply both configuration paths.
Step 1: Start a WebSocket echo server
Run an upstream that echoes every WebSocket message sent to /.ws:
docker run -d \
--name websocket-server \
-p 8080:8080 \
jmalloc/echo-serverExpected: Docker prints the new container ID.
What changed: Docker started the example upstream and published it on local port 8080.
Step 2: Create a service with the echo upstream
Create the service that sends requests to the Docker host:
curl -ks "${API7_ADMIN_URL}/apisix/admin/services/websocket-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "websocket-service",
"labels": {"guide": "websocket-proxy"},
"upstream": {
"name": "default",
"type": "roundrobin",
"scheme": "http",
"nodes": [{"host": "host.docker.internal", "port": 8080, "weight": 100}]
}
}'{ "value": { "id": "websocket-service", "name": "websocket-service", … } }What changed: the service now points to the WebSocket echo server; no route points to the service yet.
Step 3: Create a WebSocket route
Create a GET route for /.ws and enable WebSocket proxying:
curl -ks "${API7_ADMIN_URL}/apisix/admin/routes/websocket-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "websocket-route",
"labels": {"guide": "websocket-proxy"},
"paths": ["/.ws"],
"methods": ["GET"],
"service_id": "websocket-service",
"enable_websocket": true
}'{ "value": { "id": "websocket-route", "name": "websocket-route", "enable_websocket": true, … } }What changed: GET /.ws now forwards WebSocket Upgrade headers to the selected service.
Step 4: Apply the same configuration with ADC
Skip this alternative if you completed steps 2–3. Save the complete desired state:
services:
- name: websocket-service
labels:
guide: websocket-proxy
upstream:
name: default
scheme: http
nodes:
- host: host.docker.internal
port: 8080
weight: 100
routes:
- name: websocket-route
labels:
guide: websocket-proxy
uris:
- /.ws
methods:
- GET
enable_websocket: trueLint locally, ask the backend to validate the file, and inspect the diff:
adc lint -f websocket-adc.yaml
adc validate -f websocket-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=websocket-proxy
adc diff -f websocket-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=websocket-proxyExpected: lint and validation succeed, and the diff creates only the service and its nested route. The selector scopes reconciliation to top-level resources labeled guide=websocket-proxy; the route remains part of its selected service.
After reviewing the diff, apply it:
adc sync -f websocket-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=websocket-proxyExpected: ADC applies only the changes shown for the guide=websocket-proxy partition.
What changed: ADC reconciled the reviewed desired state within the selected label partition.
Validate
Send a valid HTTP Upgrade request. The key below is the RFC 6455 example nonce and decodes to the required 16 bytes. Standards-compliant WebSocket clients generate a fresh random 16-byte nonce for each connection; the fixed RFC value keeps this manual handshake reproducible. The time limits prevent a failed connection attempt or a successful long-lived upgrade from waiting indefinitely.
curl -isS --connect-timeout 2 --max-time 3 -N "${GATEWAY_URL}/.ws" \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
-H 'Sec-WebSocket-Version: 13'HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Expected: the response includes status 101 and the shown Sec-WebSocket-Accept value. After printing the handshake, curl can report error 28 when the three-second limit closes the upgraded connection; that timeout is expected for this bounded check. This curl request verifies the HTTP Upgrade handshake; it does not send or validate WebSocket data frames.
Roll back / Clean up
Use the cleanup path that matches how you created the gateway resources, then remove the example container.
Delete the route before its service:
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/websocket-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
curl -ks -o /dev/null -w '%{http_code}\n' -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/websocket-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"Expected: each command prints 200.
If either deletion does not return 200, query that exact route or service ID again and resolve the failure before removing the echo server. Do not broaden the deletion to other resources.
Save an empty desired state for this guide's label partition:
services: []Lint it and review the deletions before applying them:
adc lint -f websocket-cleanup.yaml
adc diff -f websocket-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=websocket-proxyExpected: the diff deletes only the service and nested route created under guide=websocket-proxy. Stop if it lists anything else.
adc sync -f websocket-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=websocket-proxyExpected: ADC removes only the reviewed resources in this label partition.
After the gateway resources are gone, remove the sample upstream and only the local files created for this example:
docker rm -f websocket-server
rm -f websocket-adc.yaml websocket-cleanup.yaml diff.yamlExpected: Docker prints websocket-server; the file removal produces no output.
After cleanup, this example route no longer handles /.ws. Another route can still match that path, so the data-plane response is not guaranteed to be 404.
Troubleshoot
| Symptom | Cause | Fix |
|---|---|---|
404. | The route is absent from this gateway instance or belongs to another gateway group. | Verify API7_GATEWAY_GROUP_ID, confirm an instance is Healthy, and retry after propagation. |
502 Bad Gateway. | The gateway cannot reach the echo server at host.docker.internal:8080. | Confirm docker container inspect websocket-server succeeds, then use an upstream address reachable from the gateway if it is outside Docker Desktop. |
The request does not return 101, or Sec-WebSocket-Accept differs. | The route did not forward the Upgrade handshake, or the request key changed. | Confirm enable_websocket: true, send the exact 16-byte example key, and inspect the response headers again. |
Related
- Configure gRPC proxying — proxy gRPC traffic through API7 Gateway.
- Services and routes — understand how the two gateway resources in this example relate.
- Update a route — review the API contract for
enable_websocketand the route request body. - WebSocket protocol, RFC 6455 — review the external handshake and framing specification.