I'm trying to use Envoy proxy to route requests to Service A or Service B based on the POST request bodies. For example, we care about food in the POST /v2/meal. If the request body has "food":"fruit", route the request to Service A. If the request body has "food":"soup", route the request to Service B.
Implementation
I added json_to_metadata_filter (very little example and documentations ☹️) to my proxy service. For logging, I added meta_data: "%DYNAMIC_METADATA(envoy.lb)%" to the access log.
resources:
- name: listener_0
"@type": type.googleapis.com/envoy.config.listener.v3.Listener
traffic_direction: INBOUND
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
access_log: # log out the requests
- name: envoy.access_loggers.typed_json
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
typed_json_format:
message: "requestLog"
# other fields are omitted
meta_data: "%DYNAMIC_METADATA(envoy.lb)%" # Todo: why null?
http_filters:
- name: envoy.filters.http.json_to_metadata
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.json_to_metadata.v3.JsonToMetadata
request_rules:
rules:
- selectors:
- key: food
on_present:
metadata_namespace: envoy.lb
key: food
on_missing:
metadata_namespace: envoy.lb
key: default
value: "foodMissing"
preserve_existing_metadata_value: true
on_error:
metadata_namespace: envoy.lb
key: default
value: "foodError"
preserve_existing_metadata_value: true
- name: envoy.filters.http.router # must be the last filter in a http filter chain
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
Then in the route definitions (we use envoy_data_plane to build the routes in Python), I added MetadataMatcher to make it match the food value in the metadata of each request:
# Route based on food from the request body
metadata_matchers = []
if route_data.get("food"):
food = route_data.get("food")
metadata_matchers.append(MetadataMatcher(
filter="envoy.lb",
path=[
MetadataMatcherPathSegment("food")
],
value=ValueMatcher(
string_match=StringMatcher(
exact=food,
ignore_case=True
)
)
))
route_match = RouteMatch(
dynamic_metadata=metadata_matchers, <-- matching the metadata
prefix=request_path_prefix,
case_sensitive=False,
headers=request_headers,
query_parameters=query_parameters
)
And finally my route is configured as
ROUTES = [
{
"prefix": "/v2/meal",
"food": "fruit",
"config": SERVICE_A
},
{
"prefix": "/v2/meal",
"food": "soup",
"config": SERVICE_B
},
]
The Problem
When I test the proxy with curl -X POST localhost:8080/v2/meal --data-raw '{"food":"fruit"}', I got 404 and Envoy routed the request to a default cluster Service C instead of Service A. This happened because there's no match at all and Service C doesn't have a /v2/meal endpoint. And the json_to_metadata filter didn't populate meta_data either -- see "meta_data":null below:
{"response_code":404,"request_method":"POST","trace_id":null,"user_agent":"curl/8.4.0","message":"requestLog","protocol":"HTTP/1.1","authority":"localhost:8080","full_path":"/v2/meal","bytes_sent":0,"timestamp":"2024-02-12T22:14:36.573Z","request_id":"fe632ad6-f9a7-4349-a6bc-22fd09e76b5d","duration":285,"cluster":"SERVICE_C","meta_data":null,"upstream_service_time":null,"bytes_received":164,"response_flags":"-"}
Could you help me find out what I did wrong when I use json_to_metadata to parse food from the request body and route requests based on food?
Answering my own question here. So it worked after I added
-H 'content-type: application/json'to my test POST request. Thejson_to_metadataconfig itself has been correct but it expect the request body type to be JSON.