Skip to content

Commit 5903a10

Browse files
committed
Support for removing JSON attributes from response bodies in MVC
Signed-off-by: raccoonback <[email protected]>
1 parent ec07cb8 commit 5903a10

File tree

2 files changed

+264
-1
lines changed

2 files changed

+264
-1
lines changed

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2023 the original author or authors.
2+
* Copyright 2013-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -24,6 +24,11 @@
2424
import java.util.function.Consumer;
2525
import java.util.regex.Pattern;
2626

27+
import com.fasterxml.jackson.core.JsonProcessingException;
28+
import com.fasterxml.jackson.databind.JsonNode;
29+
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import com.fasterxml.jackson.databind.node.ObjectNode;
31+
2732
import org.springframework.cloud.gateway.server.mvc.common.HttpStatusHolder;
2833
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
2934
import org.springframework.cloud.gateway.server.mvc.handler.GatewayServerResponse;
@@ -34,8 +39,16 @@
3439
import org.springframework.web.servlet.function.ServerRequest;
3540
import org.springframework.web.servlet.function.ServerResponse;
3641

42+
import static org.springframework.http.MediaType.APPLICATION_JSON;
43+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
44+
45+
/**
46+
* @author raccoonback
47+
*/
3748
public abstract class AfterFilterFunctions {
3849

50+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
51+
3952
private AfterFilterFunctions() {
4053
}
4154

@@ -160,6 +173,38 @@ public static BiFunction<ServerRequest, ServerResponse, ServerResponse> setStatu
160173
};
161174
}
162175

176+
public static BiFunction<ServerRequest, ServerResponse, ServerResponse> removeJsonAttributesResponseBody(
177+
List<String> fieldList, boolean deleteRecursively) {
178+
List<String> immutableFieldList = List.copyOf(fieldList);
179+
180+
return modifyResponseBody(String.class, String.class, APPLICATION_JSON_VALUE, (request, response, body) -> {
181+
String responseBody = body;
182+
if (APPLICATION_JSON.isCompatibleWith(response.headers().getContentType())) {
183+
try {
184+
JsonNode jsonBodyContent = OBJECT_MAPPER.readValue(responseBody, JsonNode.class);
185+
186+
removeJsonAttributes(jsonBodyContent, immutableFieldList, deleteRecursively);
187+
188+
responseBody = OBJECT_MAPPER.writeValueAsString(jsonBodyContent);
189+
}
190+
catch (JsonProcessingException exception) {
191+
throw new IllegalStateException("Failed to process JSON of response body.", exception);
192+
}
193+
}
194+
195+
return responseBody;
196+
});
197+
}
198+
199+
private static void removeJsonAttributes(JsonNode jsonNode, List<String> fieldNames, boolean deleteRecursively) {
200+
if (jsonNode instanceof ObjectNode objectNode) {
201+
objectNode.remove(fieldNames);
202+
}
203+
if (deleteRecursively) {
204+
jsonNode.forEach(childNode -> removeJsonAttributes(childNode, fieldNames, true));
205+
}
206+
}
207+
163208
public enum DedupeStrategy {
164209

165210
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.server.mvc.filter;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.beans.factory.annotation.Autowired;
25+
import org.springframework.boot.SpringBootConfiguration;
26+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.cloud.gateway.server.mvc.test.HttpbinTestcontainers;
29+
import org.springframework.cloud.gateway.server.mvc.test.HttpbinUriResolver;
30+
import org.springframework.cloud.gateway.server.mvc.test.TestLoadBalancerConfig;
31+
import org.springframework.cloud.gateway.server.mvc.test.client.TestRestClient;
32+
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.http.HttpStatus;
35+
import org.springframework.http.MediaType;
36+
import org.springframework.http.ResponseEntity;
37+
import org.springframework.test.context.ContextConfiguration;
38+
import org.springframework.web.bind.annotation.ControllerAdvice;
39+
import org.springframework.web.bind.annotation.ExceptionHandler;
40+
import org.springframework.web.servlet.function.RouterFunction;
41+
import org.springframework.web.servlet.function.ServerResponse;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
45+
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.modifyResponseBody;
46+
import static org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions.removeJsonAttributesResponseBody;
47+
import static org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions.route;
48+
import static org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions.http;
49+
50+
/**
51+
* @author raccoonback
52+
*/
53+
@SpringBootTest(webEnvironment = RANDOM_PORT)
54+
@ContextConfiguration(initializers = HttpbinTestcontainers.class)
55+
class AfterFilterFunctionsTests {
56+
57+
@Autowired
58+
TestRestClient restClient;
59+
60+
@Test
61+
void doesNotRemoveJsonAttributes() {
62+
restClient.get()
63+
.uri("/anything/does_not/remove_json_attributes")
64+
.exchange()
65+
.expectStatus()
66+
.isOk()
67+
.expectBody(Map.class)
68+
.consumeWith(res -> {
69+
assertThat(res.getResponseBody()).containsEntry("foo", "bar");
70+
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
71+
});
72+
}
73+
74+
@Test
75+
void removeJsonAttributesToAvoidBeingRecursive() {
76+
restClient.get()
77+
.uri("/anything/remove_json_attributes_to_avoid_being_recursive")
78+
.exchange()
79+
.expectStatus()
80+
.isOk()
81+
.expectBody(Map.class)
82+
.consumeWith(res -> {
83+
assertThat(res.getResponseBody()).doesNotContainKey("foo");
84+
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
85+
});
86+
}
87+
88+
@Test
89+
void removeJsonAttributesRecursively() {
90+
restClient.get()
91+
.uri("/anything/remove_json_attributes_recursively")
92+
.exchange()
93+
.expectStatus()
94+
.isOk()
95+
.expectBody(Map.class)
96+
.consumeWith(res -> {
97+
assertThat(res.getResponseBody()).containsKey("foo");
98+
assertThat((Map<String, String>) res.getResponseBody().get("foo")).containsEntry("bar", "A");
99+
assertThat(res.getResponseBody()).containsEntry("quux", "C");
100+
assertThat(res.getResponseBody()).doesNotContainKey("qux");
101+
});
102+
}
103+
104+
@Test
105+
void raisedErrorrWhenRemoveJsonAttributes() {
106+
restClient.get()
107+
.uri("/anything/raised_error_when_remove_json_attributes")
108+
.exchange()
109+
.expectStatus()
110+
.is5xxServerError()
111+
.expectBody(String.class)
112+
.consumeWith(res -> {
113+
assertThat(res.getResponseBody()).isEqualTo("Failed to process JSON of response body.");
114+
});
115+
}
116+
117+
@SpringBootConfiguration
118+
@EnableAutoConfiguration
119+
@LoadBalancerClient(name = "httpbin", configuration = TestLoadBalancerConfig.Httpbin.class)
120+
protected static class TestConfiguration {
121+
122+
@Bean
123+
public RouterFunction<ServerResponse> doesNotRemoveJsonAttributes() {
124+
// @formatter:off
125+
return route("does_not_remove_json_attributes")
126+
.GET("/anything/does_not/remove_json_attributes", http())
127+
.before(new HttpbinUriResolver())
128+
.after(
129+
removeJsonAttributesResponseBody(List.of("quux"), true)
130+
)
131+
.after(
132+
modifyResponseBody(
133+
String.class,
134+
String.class,
135+
MediaType.APPLICATION_JSON_VALUE,
136+
(request, response, s) -> "{\"foo\": \"bar\", \"baz\": \"qux\"}"
137+
)
138+
)
139+
.build();
140+
// @formatter:on
141+
}
142+
143+
@Bean
144+
public RouterFunction<ServerResponse> removeJsonAttributesToAvoidBeingRecursively() {
145+
// @formatter:off
146+
return route("remove_json_attributes_to_avoid_being_recursive")
147+
.GET("/anything/remove_json_attributes_to_avoid_being_recursive", http())
148+
.before(new HttpbinUriResolver())
149+
.after(
150+
removeJsonAttributesResponseBody(List.of("foo"), false)
151+
)
152+
.after(
153+
modifyResponseBody(
154+
String.class,
155+
String.class,
156+
MediaType.APPLICATION_JSON_VALUE,
157+
(request, response, s) -> "{\"foo\": \"bar\", \"baz\": \"qux\"}"
158+
)
159+
)
160+
.build();
161+
// @formatter:on
162+
}
163+
164+
@Bean
165+
public RouterFunction<ServerResponse> removeJsonAttributesRecursively() {
166+
// @formatter:off
167+
return route("remove_json_attributes_recursively")
168+
.GET("/anything/remove_json_attributes_recursively", http())
169+
.before(new HttpbinUriResolver())
170+
.after(
171+
removeJsonAttributesResponseBody(List.of("qux"), true)
172+
)
173+
.after(
174+
modifyResponseBody(
175+
String.class,
176+
String.class,
177+
MediaType.APPLICATION_JSON_VALUE,
178+
(request, response, s) -> "{\"foo\": { \"bar\": \"A\", \"qux\": \"B\"}, \"quux\": \"C\", \"qux\": {\"corge\": \"D\"}}"
179+
)
180+
)
181+
.build();
182+
// @formatter:on
183+
}
184+
185+
@Bean
186+
public RouterFunction<ServerResponse> raisedErrorWhenRemoveJsonAttributes() {
187+
// @formatter:off
188+
return route("raised_error_when_remove_json_attributes")
189+
.GET("/anything/raised_error_when_remove_json_attributes", http())
190+
.before(new HttpbinUriResolver())
191+
.after(
192+
removeJsonAttributesResponseBody(List.of("qux"), true)
193+
)
194+
.after(
195+
modifyResponseBody(
196+
String.class,
197+
String.class,
198+
MediaType.APPLICATION_JSON_VALUE,
199+
(request, response, s) -> "{\"invalid_json\": 123"
200+
)
201+
)
202+
.build();
203+
// @formatter:on
204+
}
205+
206+
@ControllerAdvice
207+
public class GlobalExceptionHandler {
208+
209+
@ExceptionHandler(IllegalStateException.class)
210+
public ResponseEntity<String> handleIllegalException(IllegalStateException ex) {
211+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
212+
}
213+
214+
}
215+
216+
}
217+
218+
}

0 commit comments

Comments
 (0)