Browse Source

Adds AfterFilterFunctions.dedupeResponseHeader()

See gh-2949
pull/3006/head
sgibb 1 year ago
parent
commit
11ef20d6f2
No known key found for this signature in database
GPG Key ID: 7788A47380690861
  1. 67
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/AfterFilterFunctions.java
  2. 11
      spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java
  3. 36
      spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

67
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/AfterFilterFunctions.java

@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.server.mvc.filter; @@ -18,6 +18,7 @@ package org.springframework.cloud.gateway.server.mvc.filter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
@ -25,7 +26,10 @@ import java.util.regex.Pattern; @@ -25,7 +26,10 @@ import java.util.regex.Pattern;
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.handler.GatewayServerResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
@ -43,6 +47,50 @@ public abstract class AfterFilterFunctions { @@ -43,6 +47,50 @@ public abstract class AfterFilterFunctions {
};
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> dedupeResponseHeader(String name) {
return dedupeResponseHeader(name, DedupeStrategy.RETAIN_FIRST);
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> dedupeResponseHeader(String name,
DedupeStrategy strategy) {
Assert.hasText(name, "name must not be null or empty");
Assert.notNull(strategy, "strategy must not be null");
return (request, response) -> {
dedupeHeaders(response.headers(), name, strategy);
return response;
};
}
private static void dedupeHeaders(HttpHeaders headers, String names, DedupeStrategy strategy) {
if (headers == null || names == null || strategy == null) {
return;
}
String[] tokens = StringUtils.tokenizeToStringArray(names, " ", true, true);
for (String name : tokens) {
dedupeHeader(headers, name.trim(), strategy);
}
}
private static void dedupeHeader(HttpHeaders headers, String name, DedupeStrategy strategy) {
List<String> values = headers.get(name);
if (values == null || values.size() <= 1) {
return;
}
switch (strategy) {
case RETAIN_FIRST:
headers.set(name, values.get(0));
break;
case RETAIN_LAST:
headers.set(name, values.get(values.size() - 1));
break;
case RETAIN_UNIQUE:
headers.put(name, new ArrayList<>(new LinkedHashSet<>(values)));
break;
default:
break;
}
}
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> removeResponseHeader(String name) {
return (request, response) -> {
response.headers().remove(name);
@ -90,4 +138,23 @@ public abstract class AfterFilterFunctions { @@ -90,4 +138,23 @@ public abstract class AfterFilterFunctions {
};
}
public enum DedupeStrategy {
/**
* Default: Retain the first value only.
*/
RETAIN_FIRST,
/**
* Retain the last value only.
*/
RETAIN_LAST,
/**
* Retain all unique values in the order of their first encounter.
*/
RETAIN_UNIQUE
}
}

11
spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/FilterFunctions.java

@ -24,6 +24,7 @@ import java.util.Collection; @@ -24,6 +24,7 @@ import java.util.Collection;
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
import org.springframework.cloud.gateway.server.mvc.common.KeyValues;
import org.springframework.cloud.gateway.server.mvc.common.Shortcut;
import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.util.Assert;
@ -60,6 +61,16 @@ public interface FilterFunctions { @@ -60,6 +61,16 @@ public interface FilterFunctions {
return ofResponseProcessor(AfterFilterFunctions.addResponseHeader(name, values));
}
static HandlerFilterFunction<ServerResponse, ServerResponse> dedupeResponseHeader(String name) {
return ofResponseProcessor(AfterFilterFunctions.dedupeResponseHeader(name));
}
@Shortcut
static HandlerFilterFunction<ServerResponse, ServerResponse> dedupeResponseHeader(String name,
DedupeStrategy strategy) {
return ofResponseProcessor(AfterFilterFunctions.dedupeResponseHeader(name, strategy));
}
@Shortcut
static HandlerFilterFunction<ServerResponse, ServerResponse> mapRequestHeader(String fromHeader, String toHeader) {
return ofRequestProcessor(BeforeFilterFunctions.mapRequestHeader(fromHeader, toHeader));

36
spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java

@ -69,7 +69,11 @@ import org.springframework.web.servlet.function.ServerRequest; @@ -69,7 +69,11 @@ import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy.RETAIN_FIRST;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy.RETAIN_LAST;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.DedupeStrategy.RETAIN_UNIQUE;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.addResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.dedupeResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.removeResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.rewriteResponseHeader;
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.setResponseHeader;
@ -476,6 +480,15 @@ public class ServerMvcIntegrationTests { @@ -476,6 +480,15 @@ public class ServerMvcIntegrationTests {
});
}
@Test
public void dedupeResponseHeaderWorks() {
restClient.get().uri("/headers").header("Host", "www.deduperesponseheader.org").exchange().expectStatus().isOk()
.expectHeader().valueEquals("Access-Control-Allow-Credentials", "true").expectHeader()
.valueEquals("Access-Control-Allow-Origin", "https://example.org").expectHeader()
.valueEquals("Scout-Cookie", "S'mores").expectHeader()
.valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42");
}
@Test
public void addRequestHeadersIfNotPresentWorks() {
restClient.get().uri("/headers").header("Host", "www.addrequestheadersifnotpresent.org")
@ -881,6 +894,29 @@ public class ServerMvcIntegrationTests { @@ -881,6 +894,29 @@ public class ServerMvcIntegrationTests {
// @formatter:on
}
@Bean
public RouterFunction<ServerResponse> gatewayRouterFunctionsDedupeResponseHeader() {
// @formatter:off
return route("testdeduperesponseheader")
.route(GET("/headers").and(host("{sub}.deduperesponseheader.org")), http())
.filter(new HttpbinUriResolver())
.after(dedupeResponseHeader("Access-Control-Allow-Credentials Access-Control-Allow-Origin", RETAIN_FIRST))
.after(dedupeResponseHeader("Scout-Cookie", RETAIN_LAST))
.after(dedupeResponseHeader("Next-Week-Lottery-Numbers", RETAIN_UNIQUE))
.after(addResponseHeader("Access-Control-Allow-Credentials", "false"))
.after(setResponseHeader("Access-Control-Allow-Credentials", "true"))
.after(addResponseHeader("Access-Control-Allow-Origin", "*"))
.after(setResponseHeader("Access-Control-Allow-Origin", "https://example.org"))
.after(addResponseHeader("Scout-Cookie", "S'mores"))
.after(setResponseHeader("Scout-Cookie", "Thin Mints"))
.after(addResponseHeader("Next-Week-Lottery-Numbers", "42"))
.after(addResponseHeader("Next-Week-Lottery-Numbers", "2"))
.after(addResponseHeader("Next-Week-Lottery-Numbers", "2"))
.after(setResponseHeader("Next-Week-Lottery-Numbers", "4"))
.build();
// @formatter:on
}
}
@RestController

Loading…
Cancel
Save