Docs
API7 GatewayManage traffic and APIsTraffic ManagementRewrite Upstream Responses

Rewrite Upstream Responses

Rewrite response status, body, and headers, including conditional header changes.

Applies to:API7 Gateway 3.10.x

Roles:Platform engineer

Goal

Create two independent routes on one HTTPS httpbin service. The first replaces the upstream status, body, and headers. The second adds a header only when the upstream response status is 404. Different public paths keep both examples active without route competition.

Before you begin

Set the variables used in this guide:

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
  • Confirm that the control plane and selected gateway group are ready:

    curl -ksS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}\n' "${API7_ADMIN_URL}/api/gateway_groups" -H "X-API-KEY: ${API7_ADMIN_KEY}"
    curl -ksS --connect-timeout 3 --max-time 10 "${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: 200, then 1 or more.

  • The gateway can resolve and reach httpbin.org:443. This is a public demonstration service, so its availability and behavior are outside API7 Gateway's control.

  • curl, jq, grep, and tr are available.

  • For ADC, confirm label-selector support and reserve the working files:

    adc --version
    adc sync --help | grep -- '--label-selector'
    for file in response-rewrite-adc.yaml response-rewrite-cleanup.yaml; do test ! -e "${file}" || { echo "Stop: ${file} already exists"; exit 1; }; done

    ADC resolves --gateway-group by display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified by API7_GATEWAY_GROUP_ID.

This guide creates one service and two routes. Check every fixed Admin API identifier before using PUT:

curl -ksS --connect-timeout 3 --max-time 10 -w '\nservice: %{http_code}\n' "${API7_ADMIN_URL}/apisix/admin/services/response-rewrite-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
for id in response-rewrite-body-id response-rewrite-conditional-id; do
  curl -ksS --connect-timeout 3 --max-time 10 -w "\nroute ${id}: %{http_code}\n" "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
done

Expected: every resource is absent. A 200 response containing a resource means the ID is in use; stop or change every occurrence. Correct authentication, group, and request errors until absence is unambiguous because PUT replaces a resource at the same ID. The Admin API URL IDs deliberately differ from the name fields below. ADC also treats a display name and backend ID as separate fields; do not infer one from the other. For ADC, continue only if its diff shows creates for exactly the labeled resources in this guide.

Steps

Complete steps 1 and 2 for the Admin API path, or skip them and complete step 3 for ADC. Do not apply both paths.

Step 1: Create the upstream service

curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/services/response-rewrite-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "name": "response-rewrite-guide-service",
    "labels": {"guide": "response-rewrite"},
    "upstream": {
      "name": "default", "type": "roundrobin", "scheme": "https", "pass_host": "node",
      "nodes": [{"host": "httpbin.org", "port": 443, "weight": 100}]
    }
  }'

What changed: the service points to the HTTPS demonstration upstream. No public route uses it yet.

Step 2: Create the body and conditional routes

curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes/response-rewrite-body-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "name":"response-rewrite-body","labels":{"guide":"response-rewrite"},"paths":["/response/body"],"methods":["GET"],"service_id":"response-rewrite-guide-service-id",
    "plugins":{
      "proxy-rewrite":{"uri":"/response-headers?X-Upstream-Only=remove-me"},
      "response-rewrite":{"status_code":201,"body":"{\"message\":\"rewritten response\"}","headers":{"add":["X-Added-By: response-rewrite"],"remove":["X-Upstream-Only"],"set":{"X-Rewritten-By":"API7 Gateway","Content-Type":"application/json"}}}
    }
  }'

curl -ksS --connect-timeout 3 --max-time 15 "${API7_ADMIN_URL}/apisix/admin/routes/response-rewrite-conditional-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
  -H "X-API-KEY: ${API7_ADMIN_KEY}" -H "Content-Type: application/json" \
  -d '{
    "name":"response-rewrite-conditional","labels":{"guide":"response-rewrite"},"paths":["/response/conditional/*"],"methods":["GET"],"service_id":"response-rewrite-guide-service-id",
    "plugins":{"proxy-rewrite":{"regex_uri":["^/response/conditional/(.*)","/status/$1"]},"response-rewrite":{"vars":[["status","==",404]],"headers":{"set":{"X-Conditional-Rewrite":"matched"}}}}
  }'

What changed: /response/body now asks httpbin to return X-Upstream-Only, converts its 200 response to 201, replaces its body, adds and sets headers, and removes that upstream header. /response/conditional/{status} adds a header only when the upstream status is 404. Response-header actions run in add, remove, then set order. A configured body replaces the upstream body and cannot be combined with the plugin's body filters.

Step 3: Apply the complete desired state with ADC

Skip this alternative if you completed steps 1 and 2. Save the service and both routes in one file so each sync describes the complete selected state.

response-rewrite-adc.yaml
services:
  - name: response-rewrite-guide-service
    labels: {guide: response-rewrite}
    upstream:
      name: default
      scheme: https
      pass_host: node
      nodes:
        - {host: httpbin.org, port: 443, weight: 100}
    routes:
      - name: response-rewrite-body
        labels: {guide: response-rewrite}
        uris: [/response/body]
        methods: [GET]
        plugins:
          proxy-rewrite: {uri: '/response-headers?X-Upstream-Only=remove-me'}
          response-rewrite:
            status_code: 201
            body: '{"message":"rewritten response"}'
            headers:
              add: ['X-Added-By: response-rewrite']
              remove: [X-Upstream-Only]
              set: {X-Rewritten-By: API7 Gateway, Content-Type: application/json}
      - name: response-rewrite-conditional
        labels: {guide: response-rewrite}
        uris: [/response/conditional/*]
        methods: [GET]
        plugins:
          proxy-rewrite:
            regex_uri: ['^/response/conditional/(.*)', '/status/$1']
          response-rewrite:
            vars:
              - [status, '==', 404]
            headers:
              set: {X-Conditional-Rewrite: matched}

Lint, validate, inspect, and then apply the reviewed diff:

adc lint -f response-rewrite-adc.yaml
adc validate -f response-rewrite-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=response-rewrite
adc diff -f response-rewrite-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=response-rewrite
adc sync -f response-rewrite-adc.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=response-rewrite

Expected: lint and validation succeed; the diff creates only the labeled service and its two labeled routes; sync affects only that reviewed set.

Validate

Verify the status, body, and response-header changes. The upstream URI configured on the route asks httpbin to return X-Upstream-Only, which the response plugin removes:

curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '%{http_code}\n' "${GATEWAY_URL}/response/body"
curl -fsS --connect-timeout 3 --max-time 15 "${GATEWAY_URL}/response/body" | jq -er '.message == "rewritten response"'

BODY_HEADERS=$(curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/response/body" | tr -d '\r')
printf '%s\n' "${BODY_HEADERS}" | grep -qi '^X-Added-By: response-rewrite$'
printf '%s\n' "${BODY_HEADERS}" | grep -qi '^X-Rewritten-By: API7 Gateway$'
if printf '%s\n' "${BODY_HEADERS}" | grep -qi '^X-Upstream-Only:'; then echo 'X-Upstream-Only was not removed'; exit 1; else echo 'X-Upstream-Only removed'; fi

Expected: the status is 201, jq prints true, both configured response headers are present, and the upstream-only header is absent.

Verify the conditional rule against two upstream statuses:

curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/response/conditional/404" | tr -d '\r' | grep -qi '^X-Conditional-Rewrite: matched$'
if curl -sS --connect-timeout 3 --max-time 15 -D - -o /dev/null "${GATEWAY_URL}/response/conditional/200" | tr -d '\r' | grep -qi '^X-Conditional-Rewrite:'; then echo 'Unexpected conditional header'; exit 1; else echo '200 response left unchanged'; fi
curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '404 route: %{http_code}\n' "${GATEWAY_URL}/response/conditional/404"
curl -sS --connect-timeout 3 --max-time 15 -o /dev/null -w '200 route: %{http_code}\n' "${GATEWAY_URL}/response/conditional/200"

Expected: the header is present only on the 404 response; the final commands print 404 route: 404 and 200 route: 200. The status variable is evaluated against the upstream response status before any configured status rewrite is applied.

Roll back / Clean up

For the Admin API path, delete routes before their service:

for id in response-rewrite-conditional-id response-rewrite-body-id; do
  curl -ksS --connect-timeout 3 --max-time 15 -X DELETE "${API7_ADMIN_URL}/apisix/admin/routes/${id}?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"
done
curl -ksS --connect-timeout 3 --max-time 15 -X DELETE "${API7_ADMIN_URL}/apisix/admin/services/response-rewrite-guide-service-id?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -H "X-API-KEY: ${API7_ADMIN_KEY}"

For ADC, save an empty selected state and review the deletion diff before syncing it:

response-rewrite-cleanup.yaml
services: []
adc lint -f response-rewrite-cleanup.yaml
adc diff -f response-rewrite-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=response-rewrite
adc sync -f response-rewrite-cleanup.yaml --backend api7ee --server "${API7_ADMIN_URL}" --token "${API7_ADMIN_KEY}" --gateway-group "${API7_GATEWAY_GROUP_NAME}" --tls-skip-verify --label-selector guide=response-rewrite
rm -f response-rewrite-adc.yaml response-rewrite-cleanup.yaml

Expected: the reviewed diff and sync delete only this guide's labeled service and routes.

Troubleshoot

  • A request returns 404 before reaching httpbin: confirm the public path and method, then inspect the matching route's service and plugins.
  • The body route still returns 200: confirm that /response/body selected the route containing response-rewrite.status_code: 201.
  • The conditional header appears on both responses or neither: verify the regular-expression rewrite and inspect the upstream status. The condition reads the upstream response status.
  • The upstream returns an error or times out: verify DNS and outbound HTTPS access to httpbin.org:443; retry only after confirming the public service is available.
  • ADC proposes unrelated changes: stop and verify the gateway-group display name, guide=response-rewrite selector, labels, and that the file contains the complete desired state.