From 5dc60623767339b23fa978c8a4a189f952583f49 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Tue, 26 Sep 2023 08:01:34 +0200 Subject: [PATCH] Fixes #10482 - RewriteHandler with multiple HeaderPatternRules. (#10572) Reviewed the implementation of RewriteHandler, that was broken for those rules that were overriding `Rule.Handler.handle()`. The problem was that the handling was not forwarded along the chain of rules, so only the last one was applied. Now the wrapping at the constructor produces RH3(RH2(RH1(Request))), but the handling is performed from the innermost towards the outermost. In this way, the order of rules is respected, both in the wrapping at rule application, and in the `Rule.Handler` handling. Signed-off-by: Simone Bordet --- .../jetty/rewrite/RewriteCustomizer.java | 11 +- .../rewrite/handler/CookiePatternRule.java | 13 +- .../rewrite/handler/HeaderPatternRule.java | 7 +- .../rewrite/handler/HeaderRegexRule.java | 2 +- .../jetty/rewrite/handler/InvalidURIRule.java | 2 +- .../rewrite/handler/RedirectPatternRule.java | 2 +- .../rewrite/handler/RedirectRegexRule.java | 2 +- .../rewrite/handler/ResponsePatternRule.java | 2 +- .../jetty/rewrite/handler/RewriteHandler.java | 36 +++- .../eclipse/jetty/rewrite/handler/Rule.java | 88 +++++----- .../jetty/rewrite/handler/RuleContainer.java | 10 +- .../handler/HeaderPatternRuleTest.java | 1 + .../rewrite/handler/HeaderRegexRuleTest.java | 1 + .../rewrite/handler/MultipleRulesTest.java | 155 ++++++++++++++++++ 14 files changed, 266 insertions(+), 66 deletions(-) create mode 100644 jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/MultipleRulesTest.java diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/RewriteCustomizer.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/RewriteCustomizer.java index 3f1ecd3a1c67..86f251fab139 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/RewriteCustomizer.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/RewriteCustomizer.java @@ -17,6 +17,7 @@ import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.io.RuntimeIOException; +import org.eclipse.jetty.rewrite.handler.Rule; import org.eclipse.jetty.rewrite.handler.RuleContainer; import org.eclipse.jetty.server.HttpConfiguration.Customizer; import org.eclipse.jetty.server.Request; @@ -35,7 +36,7 @@ public Request customize(Request request, HttpFields.Mutable responseHeaders) try { // TODO: rule are able to complete the request/response, but customizers cannot. - Handler input = new Handler(request); + Handler input = new Input(request); return matchAndApply(input); } catch (IOException e) @@ -43,4 +44,12 @@ public Request customize(Request request, HttpFields.Mutable responseHeaders) throw new RuntimeIOException(e); } } + + private static class Input extends Rule.Handler + { + private Input(Request request) + { + super(request); + } + } } diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CookiePatternRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CookiePatternRule.java index 76cd30836efe..348d169dbef5 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CookiePatternRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/CookiePatternRule.java @@ -82,21 +82,18 @@ public void setValue(String value) @Override public Handler apply(Handler input) throws IOException { - // Check that cookie is not already set + // Check that the cookie is not already set. List cookies = Request.getCookies(input); - if (cookies != null) + for (HttpCookie cookie : cookies) { - for (HttpCookie cookie : cookies) - { - if (_name.equals(cookie.getName()) && _value.equals(cookie.getValue())) - return null; - } + if (_name.equals(cookie.getName()) && _value.equals(cookie.getValue())) + return null; } return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) throws Exception + protected boolean handle(Response response, Callback callback) throws Exception { Response.addCookie(response, HttpCookie.from(_name, _value)); return super.handle(response, callback); diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRule.java index 967d142b2083..eb83cc618095 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRule.java @@ -65,8 +65,9 @@ public boolean isAdd() } /** - * Set true to add the response header, false to put the response header.. - * @param add true to add the response header, false to put the response header. + * Use {@code true} to add the response header, {@code false} to put the response header. + * + * @param add {@code true} to add the response header, {@code false} to put the response header. */ public void setAdd(boolean add) { @@ -79,7 +80,7 @@ public Handler apply(Handler input) throws IOException return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) throws Exception + protected boolean handle(Response response, Callback callback) throws Exception { if (isAdd()) response.getHeaders().add(getHeaderName(), getHeaderValue()); diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRule.java index 603463f8fdf5..e748da9cd0f6 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRule.java @@ -80,7 +80,7 @@ protected Handler apply(Handler input, Matcher matcher) throws IOException return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) throws Exception + protected boolean handle(Response response, Callback callback) throws Exception { if (isAdd()) response.getHeaders().add(getHeaderName(), matcher.replaceAll(getHeaderValue())); diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/InvalidURIRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/InvalidURIRule.java index 4b97ba0bb19a..75408d4c886e 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/InvalidURIRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/InvalidURIRule.java @@ -97,7 +97,7 @@ private Handler apply(Handler input) return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) + protected boolean handle(Response response, Callback callback) { String message = getMessage(); if (StringUtil.isBlank(message)) diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectPatternRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectPatternRule.java index 18bf90d796bb..eb6f9f22fa31 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectPatternRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectPatternRule.java @@ -84,7 +84,7 @@ public Handler apply(Handler input) throws IOException return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) + protected boolean handle(Response response, Callback callback) { String location = getLocation(); response.setStatus(getStatusCode()); diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java index 4dc90eec382f..3ad024b84218 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RedirectRegexRule.java @@ -82,7 +82,7 @@ protected Handler apply(Handler input, Matcher matcher) throws IOException return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) + protected boolean handle(Response response, Callback callback) { String target = matcher.replaceAll(getLocation()); response.setStatus(_statusCode); diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java index 43d1db62a1e6..f977c55b4273 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/ResponsePatternRule.java @@ -84,7 +84,7 @@ public Handler apply(Handler input) throws IOException return new Handler(input) { @Override - public boolean handle(Response response, Callback callback) + protected boolean handle(Response response, Callback callback) { String message = getMessage(); if (StringUtil.isBlank(message)) diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java index c4440b068b7a..82648394030a 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RewriteHandler.java @@ -100,6 +100,14 @@ public void addRule(Rule rule) _rules.addRule(rule); } + /** + *

Removes all the rules.

+ */ + public void clear() + { + _rules.clear(); + } + /** * @see RuleContainer#getOriginalPathAttribute() */ @@ -123,14 +131,32 @@ public boolean handle(Request request, Response response, Callback callback) thr return false; Rule.Handler input = new Rule.Handler(request); - Rule.Handler output = _rules.matchAndApply(input); + Rule.Handler result = getRuleContainer().matchAndApply(input); // No rule matched, call super with the original request. - if (output == null) + if (result == null) return super.handle(request, response, callback); - // At least one rule matched, call super with the result of the rule applications. - output.setHandler(getHandler()); - return output.handle(output, response, callback); + // At least one rule matched, link the last Rule.Handler + // to invoke the child Handler of this RewriteHandler. + new LastRuleHandler(result, getHandler()); + return input.handle(response, callback); + } + + private static class LastRuleHandler extends Rule.Handler + { + private final Handler _handler; + + private LastRuleHandler(Rule.Handler ruleHandler, Handler handler) + { + super(ruleHandler); + _handler = handler; + } + + @Override + protected boolean handle(Response response, Callback callback) throws Exception + { + return _handler.handle(getWrapped(), response, callback); + } } } diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/Rule.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/Rule.java index b5d63da369e4..8710dda46435 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/Rule.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/Rule.java @@ -29,30 +29,34 @@ public abstract class Rule private boolean _terminating; /** - *

Tests whether the given {@code Request} should apply, and if so the rule logic is triggered.

+ *

Tests whether the given input {@code Handler} (which wraps a + * {@code Request}) matches the rule, and if so returns an output + * {@code Handler} that applies the rule logic.

+ *

If the input does not match, {@code null} is returned.

* - * @param input the input {@code Request} and {@code Handler} - * @return the possibly wrapped {@code Request} and {@code Handler}, or {@code null} if the rule did not match - * @throws IOException if applying the rule failed + * @param input the input {@code Handler} that wraps the {@code Request} + * @return an output {@code Handler} that wraps the input {@code Handler}, + * or {@code null} if the rule does not match + * @throws IOException if applying the rule fails */ public abstract Handler matchAndApply(Handler input) throws IOException; /** - * @return when {@code true}, rules after this one are not invoked + * @return whether rules after this one are not invoked */ public boolean isTerminating() { return _terminating; } + /** + * @param value whether rules after this one are not invoked + */ public void setTerminating(boolean value) { _terminating = value; } - /** - * Returns the handling and terminating flag values. - */ @Override public String toString() { @@ -60,63 +64,61 @@ public String toString() } /** - *

A {@link Request.Wrapper} that is also a {@link Handler}, - * used to chain a sequence of {@link Rule}s together. - * The rule handler is initialized with the initial request, then it is - * passed to a chain of rules before the child {@code Handler} is - * passed in {@link #setHandler(Handler)}. Finally, the response - * and callback are provided in a call to {@link #handle(Request, Response, Callback)}, - * which calls the {@link #handle(Response, Callback)}.

+ *

A {@link Request.Wrapper} used to chain a sequence of {@link Rule}s together.

+ *

The first {@link Rule.Handler} is initialized with the initial {@link Request}, + * then it is passed to a chain of {@link Rule}s, which in turn chain {@link Rule.Handler}s + * together. + * At the end of the {@link Rule} applications, {@link Rule.Handler}s are chained so that + * so that the first rule produces the innermost {@code Handler} and the last rule produces + * the outermost {@code Handler} in this way: {@code RH3(RH2(RH1(Req)))}.

+ *

After the {@link Rule} applications, the {@link Rule.Handler}s are then called in + * sequence, starting from the innermost and moving outwards with respect to the wrapping, + * until finally the {@link org.eclipse.jetty.server.Handler#handle(Request, Response, Callback)} + * method of the child {@code Handler} of {@link RewriteHandler} is invoked.

*/ - public static class Handler extends Request.Wrapper implements Request.Handler + public static class Handler extends Request.Wrapper { - private volatile Handler _handler; + private Rule.Handler _nextRuleHandler; - public Handler(Request request) + protected Handler(Request request) { super(request); } - @Override - public boolean handle(Request request, Response response, Callback callback) throws Exception + public Handler(Rule.Handler handler) { - return handle(response, callback); + super(handler); + handler._nextRuleHandler = this; } /** - *

Handles this wrapped request together with the passed response and - * callback, using the handler set in {@link #setHandler(Handler)}. - * This method should be extended if additional handling of the wrapped - * request is required.

- * @param response The response - * @param callback The callback - * @throws Exception If there is a problem handling - * @see #setHandler(Handler) - */ - protected boolean handle(Response response, Callback callback) throws Exception - { - Handler handler = _handler; - return handler != null && handler.handle(this, response, callback); - } - - /** - *

Wraps the given {@code Handler} within this instance and returns this instance.

+ *

Handles this wrapped request together with the passed response and callback.

+ *

This method should be overridden only if the rule applies to the response, + * or the rule completes the callback. + * By default this method forwards the handling to the next {@link Rule.Handler}. + * If a rule that overrides this method is non-{@link #isTerminating() terminating}, + * it should call the {@code super} implementation to chain the rules.

* - * @param handler the {@code Handler} to wrap + * @param response the {@link Response} + * @param callback the {@link Callback} + * @throws Exception if there is a failure while handling the rules */ - public void setHandler(Handler handler) + protected boolean handle(Response response, Callback callback) throws Exception { - _handler = handler; + return _nextRuleHandler.handle(response, callback); } } + /** + *

A {@link Rule.Handler} that wraps a {@link Request} to return a different {@link HttpURI}.

+ */ public static class HttpURIHandler extends Handler { private final HttpURI _uri; - public HttpURIHandler(Request request, HttpURI uri) + public HttpURIHandler(Rule.Handler handler, HttpURI uri) { - super(request); + super(handler); _uri = uri; } diff --git a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java index 8b6c4252db33..7e8cf48833af 100644 --- a/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java +++ b/jetty-core/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java @@ -51,7 +51,7 @@ public List getRules() */ public void setRules(List rules) { - _rules.clear(); + clear(); _rules.addAll(rules); } @@ -71,6 +71,14 @@ public void addRule(Rule rule) _rules.add(rule); } + /** + *

Removes all the rules.

+ */ + public void clear() + { + _rules.clear(); + } + /** * @return the request attribute name used to store the request original path * @see #setOriginalPathAttribute(String) diff --git a/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java index b55233066f44..177b53b0cd18 100644 --- a/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java +++ b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderPatternRuleTest.java @@ -80,6 +80,7 @@ public void testHeaderWithNumberValues() throws Exception assertEquals(200, response.getStatus()); assertEquals(value, response.get(name)); + _rewriteHandler.clear(); stop(); } } diff --git a/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java index 846ea6158482..003b7ad70d7f 100644 --- a/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java +++ b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/HeaderRegexRuleTest.java @@ -81,6 +81,7 @@ public void testHeaderWithNumberValues() throws Exception assertEquals(200, response.getStatus()); assertEquals(value, response.get(name)); + _rewriteHandler.clear(); stop(); } } diff --git a/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/MultipleRulesTest.java b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/MultipleRulesTest.java new file mode 100644 index 000000000000..beb8c9a5356d --- /dev/null +++ b/jetty-core/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/MultipleRulesTest.java @@ -0,0 +1,155 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.rewrite.handler; + +import java.util.List; + +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MultipleRulesTest extends AbstractRuleTest +{ + @Test + public void testMultipleRulesWithSamePattern() throws Exception + { + HeaderPatternRule rule1 = new HeaderPatternRule("/*", "name1", "value1"); + RewriteRegexRule rule2 = new RewriteRegexRule("/", "/rewritten"); + HeaderPatternRule rule3 = new HeaderPatternRule("/*", "name2", "value2"); + List.of(rule2, rule1, rule3).forEach(_rewriteHandler::addRule); + start(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String pathInContext = Request.getPathInContext(request); + assertEquals("/rewritten", pathInContext); + callback.succeeded(); + return true; + } + }); + + String request = """ + GET / HTTP/1.1 + Host: localhost + + """; + + HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request)); + assertEquals(200, response.getStatus()); + assertEquals("value1", response.get("name1")); + assertEquals("value2", response.get("name2")); + } + + @Test + public void testMultipleRulesAppliesAndHandledInOrder() throws Exception + { + String requestHeaderName = "X-Request-Header"; + String responseHeaderName = "X-Response-Header"; + Rule rule1 = new Rule() + { + @Override + public Handler matchAndApply(Handler input) + { + // First rule, I should only see the client headers. + String value = input.getHeaders().get(requestHeaderName); + assertEquals("Request", value); + + HttpFields newFields = HttpFields.build(input.getHeaders()).put(requestHeaderName, String.join(", ", value, "Rule1")); + return new Handler(input) + { + @Override + public HttpFields getHeaders() + { + return newFields; + } + + @Override + protected boolean handle(Response response, Callback callback) throws Exception + { + // Modify the response. + response.getHeaders().put(responseHeaderName, "Rule1"); + // Chain the rules. + return super.handle(response, callback); + } + }; + } + }; + RewriteRegexRule rule2 = new RewriteRegexRule("/", "/rewritten"); + Rule rule3 = new Rule() + { + @Override + public Handler matchAndApply(Handler input) + { + // Third rule, I should see the effects of the previous 2 rules. + String value = input.getHeaders().get(requestHeaderName); + assertEquals("Request, Rule1", value); + + String pathInContext = Request.getPathInContext(input); + assertEquals("/rewritten", pathInContext); + + HttpFields newFields = HttpFields.build(input.getHeaders()).put(requestHeaderName, String.join(", ", "Request", "Rule1", "Rule3")); + return new Handler(input) + { + @Override + public HttpFields getHeaders() + { + return newFields; + } + + @Override + protected boolean handle(Response response, Callback callback) throws Exception + { + assertEquals("Rule1", response.getHeaders().get(responseHeaderName)); + // Modify the response. + response.getHeaders().put(responseHeaderName, String.join(", ", "Rule1", "Rule3")); + // Chain the rules. + return super.handle(response, callback); + } + }; + } + }; + List.of(rule1, rule2, rule3).forEach(_rewriteHandler::addRule); + start(new Handler.Abstract() + { + @Override + public boolean handle(Request request, Response response, Callback callback) + { + String pathInContext = Request.getPathInContext(request); + assertEquals("/rewritten", pathInContext); + assertEquals("Request, Rule1, Rule3", request.getHeaders().get(requestHeaderName)); + assertEquals("Rule1, Rule3", response.getHeaders().get(responseHeaderName)); + callback.succeeded(); + return true; + } + }); + + String request = """ + GET / HTTP/1.1 + Host: localhost + %s: Request + + """.formatted(requestHeaderName); + + HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request)); + assertEquals(200, response.getStatus()); + assertEquals("Rule1, Rule3", response.get(responseHeaderName)); + } +}