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 -lExpected:
200, then1or 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, andtrare 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; }; doneADC resolves
--gateway-groupby display name; the compatibility Admin API uses the group's UUID. Enter the display name of the group identified byAPI7_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}"
doneExpected: 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.
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-rewriteExpected: 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'; fiExpected: 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:
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.yamlExpected: the reviewed diff and sync delete only this guide's labeled service and routes.
Troubleshoot
- A request returns
404before 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/bodyselected the route containingresponse-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-rewriteselector, labels, and that the file contains the complete desired state.