-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for ResponseHeaderModifier for HTTPRouteRule objects #1494
Conversation
Problem: Users want to add, set, and remove response headers. Solution: Use `add_header` NGINX directive to support `ResponseHeaderModifier`. * If the action is `set`, we simply configure the `add_header` directive with the given value in the HTTPRoute spec. * If the action is `remove`, we configure the `add_header` directive with the value set to an empty string. * If the action is `add`, we create a mapping for the `$http_header_name` variable appending a comma at the end of any client provided headers (if present) and prepend this to the given value in the HTTPRoute spec.
ca02a6d
to
e159b03
Compare
@kevin85421 thanks for working on this! We have three actions for ResponseHeaderModifier:
With your current implementation, cases 1, 2, and 3 work as expected, but the rest do not. There are a couple of reasons for that. (1) In the manual test you described, you check the request headers instead of the response headers. More on testing later. (2) The
To get all the cases passing, we can add the following nginx directives: (1) add:
==>
(2) set: we need two directives for this action:
==>
(3) remove:
==>
There's no need for To verify the functionality, run the conformance tests locally. You will need to add To iterate more quickly and run only the If you would like to test manually, ping me on Slack, and I can send you the Yaml files I used for testing. Thanks again for working on this! |
@kate-osborn Thank you for your detailed reply!
Do you mean that the following output from the
|
@kevin85421 sorry, I meant the output from the echo app:
That app writes the request headers. The curl command you used does show the response headers. |
I tried to test this PR manually. The
|
I can't easily run conformance tests locally based on this doc.
I finally decided to modify YAMLs to run the conformance manually. # Step 1: Install NGF
# Step 2: Gateway / Service / Backend server ...
# https://gist.github.com/kevin85421/1b0d21ff5c2735950da4c73bfd0bb1ee
kubectl apply -f manifests.yaml
# Step 3: HTTPRoute
# https://gist.github.com/kevin85421/41c72b2194a887edd6cf01c5bdbccda4
kubectl apply -f httproute-response-header-modifier.yaml
# Step 4: Send a request to simulate test 1 (2nd test) in httproute-response-header-modifier.go.
curl -v http://127.0.0.1:8080/set -H "X-Echo-Set-Header:Some-Other-Header:val,X-Header-Set:some-other-value"
|
Yes, you're right. I mistakenly gave you the test's ShortName instead of the SupportedFeature name.
I was able to get the HTTPRouteResponseHeaderModifier test running by adding
I haven't tried using the
We use this mode only for conformance tests because the tests create multiple Gateway resources, which the
The conformance tests clean up after themselves, so my guess is that all the resources were deleted after the tests finished, which is why you weren't able to reach any of the backends.
That's not expected. Were you running NGF in
The HTTPRouteResponseHeaderModifier test uses this echo-server as the Backend, which allows you to set the response headers for a request by specifying them in the When you sent the curl request Before, when you were testing with our header example backend, you were not setting the response headers in your curl request. Instead, you were setting the request headers. Our header example backend is not as smart as the echo-server, and there is no way to set the response headers with it.
When verifying response headers, add the
I thought I hope this helps, thanks again for your effort on this! Feel free to ping me on slack if you run into any more issues. |
ProxyPass string | ||
HTTPMatchVar string | ||
ProxySetHeaders []Header | ||
AddResponseHeaders []Header |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might consider reusing dataplane.HTTPHeaderFilter
instead of using AddResponseHeaders
, SetResponseHeaders
, and RemoveResponseHeaders
. Why do we need both Header
and dataplane.HTTPHeader
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are trying to keep the dataplane configuration as dataplane-agnostic as possible. We translate Kubernetes APIs -> dataplane configuration -> nginx configuration. Each layer is separate.
I will get the following error when I use kubectl run -i conformance \
--image=conformance-test-runner:latest --image-pull-policy=Never \
--overrides='{ "spec": { "serviceAccountName": "conformance" } }' \
--restart=Never -- sh -c "go test -v . -tags conformance,experimental -args --gateway-class=nginx \
--run TestConformance/HTTPRouteResponseHeaderModifier \
--supported-features=HTTPRouteResponseHeaderModification --version= \
--report-output=output.txt; cat output.txt" | tee output.txt
If you don't see a command prompt, try pressing enter.
flag provided but not defined: -run
I installed NGF in static mode instead of running |
Manual test make install-ngf-local-build
make run-conformance-tests
# Test passes. See https://gist.github.com/kevin85421/f9aa82a311a52e1240c70ae3f088f722 for more details. |
I followed your suggestions and removed filters:
- type: ResponseHeaderModifier
responseHeaderModifier:
add:
- name: X-Header-Add
value: add-appends-values In my expectation, the response headers should be curl -i http://127.0.0.1:8080/add -H "X-Echo-Set-Header:Some-Other-Header:val,X-Header-Add:some-other-value" |
|
I feel so embarrassed about my dumb question lol. |
@kevin85421 I believe that multiple entries of the same header name are equivalent to one header name with a comma-separated list of values, so long as the order of the headers is correct. To be sure, I asked this question in the Gateway API slack channel: https://kubernetes.slack.com/archives/CR0H13KGA/p1707260286602689. |
@kevin85421 confirmed that they are equivalent. I hope that answers your question. I will take a look at the rest of your PR this week 🙂 |
@kate-osborn Thank you! |
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
e91f5e2
to
c0f98ab
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM for the docs changes: I am not approving because most of the PR is code-based.
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
🤔 this key is in the Perhaps rebasing will solve the issue? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following up on extra validation:
Let's disallow the following special header fields in the ResponseHeaderModifier:
- "Server"
- "Date"
- "X-Pad"
- Headers starting with "X-Accel"
- "Content-Type"
- "Content-Length"
- "Connection"
After some testing, trying to modify these headers in the same way we modify other headers doesn't work. I think it's better to disallow them through validation and document in the compatibility doc that they aren't supported header values.
Add: []http.Header{}, | ||
Set: []http.Header{}, | ||
Remove: []string{}, | ||
}, | ||
}, | ||
}, | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add some unit tests for the generateResponseHeaders
function?
Let's also add to the createServers
test to cover the case where multiple compatible filters are defined. All filters should be compatible except URLRewrite and RequestRedirect.
@@ -806,16 +809,16 @@ func validateFilterRewrite( | |||
} | |||
|
|||
func validateFilterHeaderModifier( | |||
filterType v1.HTTPRouteFilterType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line of code changed in a recent commit: https://github.com/nginxinc/nginx-gateway-fabric/blob/8d5f023eb4b7bd167a98adcdfae82ad41bb20249/internal/mode/static/state/graph/httproute.go#L825
Since we now provide the path, I don't think we need to specify the child name in the error message. We can just say:
if headerModifier == nil {
return field.ErrorList{field.Required(filterPath, "cannot be nil")}
}
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
Problem: Users want to add, set, and remove response headers. Solution: Use add_header and proxy_hide_header NGINX directives to support ResponseHeaderModifier. If the action is set, we configure the proxy_hide_header directive to remove the header if exists and add_header directive with the given value in the HTTPRoute spec. If the action is remove, we configure the proxy_hide_header directive to remove the header. If the action is add, we simply configure the add_header directive with the given value in the HTTPRoute spec.
Thank @ADubhlaoich and @kate-osborn for the review! I have already addressed some comments. I have been very busy recently. I will do my best to address #1494 (review) and #1494 (comment) as soon as possible, possibly by next weekend. |
This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
I will update this PR today. |
responseHeaderModifier *v1.HTTPHeaderFilter, | ||
filterPath *field.Path, | ||
) field.ErrorList { | ||
if errList := validateFilterHeaderModifier(validator, responseHeaderModifier, filterPath); errList != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If responseHeaderModifier
is invalid for both validateFilterHeaderModifier
and disallowedResponseHeaderSet
, should we include both in allErrs
, or do we only need to return validateFilterHeaderModifier
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we only need to return the errors from validateFilterHeaderModifier
.
@pleshakov, does that seem right to you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we only need to return the errors from validateFilterHeaderModifier.
Sounds reasonable.
In the check, I would check the length of errList, not that it doesn't equal to nil - just so that we don't dependent on zero-length vs nil slice differences.
I will update the docs after #1494 (comment) got answered. |
} | ||
} | ||
for _, h := range responseHeaderModifier.Set { | ||
valErr := field.Invalid(filterPath.Child("set"), h, "header name is not allowed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend extracting a function that accepts []v1.HTTPHeader
and field.Path
and checks if the header names are allowed.
Similar to how it's done in the validateFilterHeaderModifierFields
function,
}, | ||
}, | ||
expectErrCount: 3, | ||
name: "invalid response header modifier filter", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
name: "invalid response header modifier filter", | |
name: "response header modifier filter with disallowed header name", |
}, | ||
}, | ||
expectErrCount: 3, | ||
name: "invalid response header modifier filter", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
name: "invalid response header modifier filter", | |
name: "response header modifier filter with disallowed header name prefix", |
This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 14 days. |
I have been very busy recently and may not be able to work on this PR for the next 2 weeks. Hence, I am closing this PR to avoid blocking the community's roadmap. Feel free to take over this issue. I may reopen it in the future if I find the time to work on it and if the issue remains unresolved. Sorry for wasting your time. |
Proposed changes
Problem: Users want to add, set, and remove response headers.
Solution: Use
add_header
NGINX directive to supportResponseHeaderModifier
.set
, we simply configure theadd_header
directive with the given value in the HTTPRoute spec.remove
, we configure theadd_header
directive with the value set to an empty string.add
, we create a mapping for the$http_header_name
variable appending a comma at the end of any client provided headers (if present) and prepend this to the given value in the HTTPRoute spec.Testing:
ResponseHeaderModifier
to echo-route.yaml.kubectl -n nginx-gateway port-forward $NGF_POD 8080:80 8443:443
curl
command:Closes #1397
Checklist
Before creating a PR, run through this checklist and mark each as complete.