Docs
API7 GatewayManage traffic and APIsProtocol ProxyConfigure WebSocket Proxying

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 -l

    Expected: 1 or more.

  • Docker and curl are available:

    docker version --format '{{.Client.Version}}'
    curl --version | head -1

    Expected: a Docker client version and a curl version line.

  • No container named websocket-server already exists, and no running Docker container already publishes local port 8080:

    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; fi

    Expected: 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 ADC v0.30.3; use a version whose help lists it. ADC looks up --gateway-group by gateway group display name, so API7_GATEWAY_GROUP_NAME is distinct from the UUID used by the compatibility Admin API. Enter the name of the same group identified by API7_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.yaml in 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; }; done

    Expected: 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-server

Expected: 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:

websocket-adc.yaml
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: true

Lint 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-proxy

Expected: 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-proxy

Expected: 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.

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.yaml

Expected: 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

SymptomCauseFix
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.