Skip to content

Commit ad7c7df

Browse files
committed
Refactor: Pass in responseMetaData when validating request if available
1 parent f71ad6a commit ad7c7df

File tree

8 files changed

+99
-46
lines changed

8 files changed

+99
-46
lines changed

openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidator.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.nio.charset.StandardCharsets;
1515
import java.util.concurrent.RejectedExecutionException;
1616
import java.util.concurrent.ThreadPoolExecutor;
17+
import javax.annotation.Nullable;
1718
import lombok.extern.slf4j.Slf4j;
1819
import org.apache.http.client.utils.URLEncodedUtils;
1920

@@ -46,8 +47,8 @@ public boolean isReady() {
4647
return validator != null;
4748
}
4849

49-
public void validateRequestObjectAsync(final RequestMetaData request, String requestBody) {
50-
executeAsync(() -> validateRequestObject(request, requestBody));
50+
public void validateRequestObjectAsync(final RequestMetaData request, @Nullable ResponseMetaData response, String requestBody) {
51+
executeAsync(() -> validateRequestObject(request, response, requestBody));
5152
}
5253

5354
public void validateResponseObjectAsync(final RequestMetaData request, ResponseMetaData response, final String responseBody) {
@@ -63,10 +64,18 @@ private void executeAsync(Runnable command) {
6364
}
6465

6566
public ValidationResult validateRequestObject(final RequestMetaData request, String requestBody) {
67+
return validateRequestObject(request, null, requestBody);
68+
}
69+
70+
public ValidationResult validateRequestObject(
71+
final RequestMetaData request,
72+
@Nullable final ResponseMetaData response,
73+
String requestBody
74+
) {
6675
try {
6776
var simpleRequest = buildSimpleRequest(request, requestBody);
6877
var result = validator.validateRequest(simpleRequest);
69-
validationReportHandler.handleValidationReport(request, Direction.REQUEST, requestBody, result);
78+
validationReportHandler.handleValidationReport(request, response, Direction.REQUEST, requestBody, result);
7079
reportValidationHeartbeat();
7180
return buildValidationResult(result);
7281
} catch (Exception e) {
@@ -96,7 +105,7 @@ private static String nullSafeUrlDecode(String value) {
96105

97106
public ValidationResult validateResponseObject(
98107
final RequestMetaData request,
99-
ResponseMetaData response,
108+
final ResponseMetaData response,
100109
final String responseBody
101110
) {
102111
try {
@@ -112,7 +121,7 @@ public ValidationResult validateResponseObject(
112121
Request.Method.valueOf(request.getMethod().toUpperCase()),
113122
responseBuilder.build()
114123
);
115-
validationReportHandler.handleValidationReport(request, Direction.RESPONSE, responseBody, result);
124+
validationReportHandler.handleValidationReport(request, response, Direction.RESPONSE, responseBody, result);
116125
reportValidationHeartbeat();
117126
return buildValidationResult(result);
118127
} catch (Exception e) {

openapi-validation-core/src/main/java/com/getyourguide/openapi/validation/core/ValidationReportHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import com.getyourguide.openapi.validation.api.model.Direction;
88
import com.getyourguide.openapi.validation.api.model.OpenApiViolation;
99
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
10+
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
1011
import com.getyourguide.openapi.validation.core.exclusions.InternalViolationExclusions;
1112
import com.getyourguide.openapi.validation.core.throttle.ValidationReportThrottler;
1213
import io.swagger.v3.oas.models.parameters.Parameter;
1314
import java.util.Optional;
15+
import javax.annotation.Nullable;
1416
import lombok.AllArgsConstructor;
1517

1618
@AllArgsConstructor
@@ -22,6 +24,7 @@ public class ValidationReportHandler {
2224

2325
public void handleValidationReport(
2426
RequestMetaData request,
27+
@Nullable ResponseMetaData response,
2528
Direction direction,
2629
String body,
2730
ValidationReport result
@@ -30,7 +33,7 @@ public void handleValidationReport(
3033
result
3134
.getMessages()
3235
.stream()
33-
.map(message -> buildOpenApiViolation(message, request, body, direction))
36+
.map(message -> buildOpenApiViolation(message, request, response, body, direction))
3437
.filter(violation -> !violationExclusions.isExcluded(violation))
3538
.forEach(violation -> throttleHelper.throttle(violation, () -> logValidationError(violation)));
3639
}
@@ -44,6 +47,7 @@ private void logValidationError(OpenApiViolation openApiViolation) {
4447
private OpenApiViolation buildOpenApiViolation(
4548
ValidationReport.Message message,
4649
RequestMetaData request,
50+
@Nullable ResponseMetaData response,
4751
String body,
4852
Direction direction
4953
) {

openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/OpenApiRequestValidatorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public void setup() {
4444
public void testWhenThreadPoolExecutorRejectsExecutionThenItShouldNotThrow() {
4545
Mockito.doThrow(new RejectedExecutionException()).when(threadPoolExecutor).execute(any());
4646

47-
openApiRequestValidator.validateRequestObjectAsync(mock(), null);
47+
openApiRequestValidator.validateRequestObjectAsync(mock(), null, null);
4848
}
4949

5050
@Test

openapi-validation-core/src/test/java/com/getyourguide/openapi/validation/core/ValidationReportHandlerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void testWhenParameterNameIsPresentThenItShouldAddItToTheMessage() {
4646
var request = mockRequestMetaData();
4747
var validationReport = mockValidationReport("parameterName");
4848

49-
validationReportHandler.handleValidationReport(request, Direction.REQUEST, null, validationReport);
49+
validationReportHandler.handleValidationReport(request, null, Direction.REQUEST, null, validationReport);
5050

5151
var argumentCaptor = ArgumentCaptor.forClass(OpenApiViolation.class);
5252
verify(logger).log(argumentCaptor.capture());

spring-boot-starter/spring-boot-starter-web-spring2.7/src/main/java/com/getyourguide/openapi/validation/filter/OpenApiValidationHttpFilter.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.getyourguide.openapi.validation.filter;
22

33
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
4+
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
45
import com.getyourguide.openapi.validation.api.model.ValidationResult;
56
import com.getyourguide.openapi.validation.api.selector.TrafficSelector;
67
import com.getyourguide.openapi.validation.core.OpenApiRequestValidator;
78
import com.getyourguide.openapi.validation.factory.ContentCachingWrapperFactory;
89
import com.getyourguide.openapi.validation.factory.ServletMetaDataFactory;
910
import java.io.IOException;
1011
import java.nio.charset.StandardCharsets;
12+
import javax.annotation.Nullable;
1113
import javax.servlet.FilterChain;
1214
import javax.servlet.ServletException;
1315
import javax.servlet.ServletRequest;
@@ -59,12 +61,17 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
5961
try {
6062
super.doFilter(requestWrapper, responseWrapper, chain);
6163
} finally {
64+
var responseMetaData = metaDataFactory.buildResponseMetaData(responseWrapper);
6265
if (!alreadyDidRequestValidation) {
63-
validateRequest(requestWrapper, requestMetaData, RunType.ASYNC);
66+
validateRequest(requestWrapper, requestMetaData, responseMetaData, RunType.ASYNC);
6467
}
6568

66-
var validateResponseResult =
67-
validateResponse(responseWrapper, requestMetaData, getRunTypeForResponseValidation(requestMetaData));
69+
var validateResponseResult = validateResponse(
70+
responseWrapper,
71+
requestMetaData,
72+
responseMetaData,
73+
getRunTypeForResponseValidation(requestMetaData)
74+
);
6875
throwStatusExceptionOnViolation(validateResponseResult, "Response validation failed");
6976

7077
responseWrapper.copyBodyToResponse(); // Needs to be done on every call, otherwise there won't be a response body
@@ -87,14 +94,15 @@ private boolean validateRequestWithFailOnViolation(
8794
return false;
8895
}
8996

90-
var validateRequestResult = validateRequest(request, requestMetaData, RunType.SYNC);
97+
var validateRequestResult = validateRequest(request, requestMetaData, null, RunType.SYNC);
9198
throwStatusExceptionOnViolation(validateRequestResult, "Request validation failed");
9299
return true;
93100
}
94101

95102
private ValidationResult validateRequest(
96103
ContentCachingRequestWrapper request,
97104
RequestMetaData requestMetaData,
105+
@Nullable ResponseMetaData responseMetaData,
98106
RunType runType
99107
) {
100108
if (!trafficSelector.canRequestBeValidated(requestMetaData)) {
@@ -106,7 +114,7 @@ private ValidationResult validateRequest(
106114
: null;
107115

108116
if (runType == RunType.ASYNC) {
109-
validator.validateRequestObjectAsync(requestMetaData, requestBody);
117+
validator.validateRequestObjectAsync(requestMetaData, responseMetaData, requestBody);
110118
return ValidationResult.NOT_APPLICABLE;
111119
} else {
112120
return validator.validateRequestObject(requestMetaData, requestBody);
@@ -116,9 +124,9 @@ private ValidationResult validateRequest(
116124
private ValidationResult validateResponse(
117125
ContentCachingResponseWrapper response,
118126
RequestMetaData requestMetaData,
127+
ResponseMetaData responseMetaData,
119128
RunType runType
120129
) {
121-
var responseMetaData = metaDataFactory.buildResponseMetaData(response);
122130
if (!trafficSelector.canResponseBeValidated(requestMetaData, responseMetaData)) {
123131
return ValidationResult.NOT_APPLICABLE;
124132
}

spring-boot-starter/spring-boot-starter-web/src/main/java/com/getyourguide/openapi/validation/filter/OpenApiValidationHttpFilter.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.getyourguide.openapi.validation.filter;
22

33
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
4+
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
45
import com.getyourguide.openapi.validation.api.model.ValidationResult;
56
import com.getyourguide.openapi.validation.api.selector.TrafficSelector;
67
import com.getyourguide.openapi.validation.core.OpenApiRequestValidator;
@@ -15,6 +16,7 @@
1516
import jakarta.servlet.http.HttpServletResponse;
1617
import java.io.IOException;
1718
import java.nio.charset.StandardCharsets;
19+
import javax.annotation.Nullable;
1820
import lombok.AllArgsConstructor;
1921
import lombok.extern.slf4j.Slf4j;
2022
import org.springframework.core.Ordered;
@@ -59,12 +61,17 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
5961
try {
6062
super.doFilter(requestWrapper, responseWrapper, chain);
6163
} finally {
64+
var responseMetaData = metaDataFactory.buildResponseMetaData(responseWrapper);
6265
if (!alreadyDidRequestValidation) {
63-
validateRequest(requestWrapper, requestMetaData, RunType.ASYNC);
66+
validateRequest(requestWrapper, requestMetaData, responseMetaData, RunType.ASYNC);
6467
}
6568

66-
var validateResponseResult =
67-
validateResponse(responseWrapper, requestMetaData, getRunTypeForResponseValidation(requestMetaData));
69+
var validateResponseResult = validateResponse(
70+
responseWrapper,
71+
requestMetaData,
72+
responseMetaData,
73+
getRunTypeForResponseValidation(requestMetaData)
74+
);
6875
throwStatusExceptionOnViolation(validateResponseResult, "Response validation failed");
6976

7077
responseWrapper.copyBodyToResponse(); // Needs to be done on every call, otherwise there won't be a response body
@@ -87,14 +94,15 @@ private boolean validateRequestWithFailOnViolation(
8794
return false;
8895
}
8996

90-
var validateRequestResult = validateRequest(request, requestMetaData, RunType.SYNC);
97+
var validateRequestResult = validateRequest(request, requestMetaData, null, RunType.SYNC);
9198
throwStatusExceptionOnViolation(validateRequestResult, "Request validation failed");
9299
return true;
93100
}
94101

95102
private ValidationResult validateRequest(
96103
ContentCachingRequestWrapper request,
97104
RequestMetaData requestMetaData,
105+
@Nullable ResponseMetaData responseMetaData,
98106
RunType runType
99107
) {
100108
if (!trafficSelector.canRequestBeValidated(requestMetaData)) {
@@ -106,7 +114,7 @@ private ValidationResult validateRequest(
106114
: null;
107115

108116
if (runType == RunType.ASYNC) {
109-
validator.validateRequestObjectAsync(requestMetaData, requestBody);
117+
validator.validateRequestObjectAsync(requestMetaData, responseMetaData, requestBody);
110118
return ValidationResult.NOT_APPLICABLE;
111119
} else {
112120
return validator.validateRequestObject(requestMetaData, requestBody);
@@ -116,9 +124,9 @@ private ValidationResult validateRequest(
116124
private ValidationResult validateResponse(
117125
ContentCachingResponseWrapper response,
118126
RequestMetaData requestMetaData,
127+
ResponseMetaData responseMetaData,
119128
RunType runType
120129
) {
121-
var responseMetaData = metaDataFactory.buildResponseMetaData(response);
122130
if (!trafficSelector.canResponseBeValidated(requestMetaData, responseMetaData)) {
123131
return ValidationResult.NOT_APPLICABLE;
124132
}

spring-boot-starter/spring-boot-starter-webflux-spring2.7/src/main/java/com/getyourguide/openapi/validation/filter/OpenApiValidationWebFilter.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.getyourguide.openapi.validation.filter;
22

33
import com.getyourguide.openapi.validation.api.model.RequestMetaData;
4+
import com.getyourguide.openapi.validation.api.model.ResponseMetaData;
45
import com.getyourguide.openapi.validation.api.model.ValidationResult;
56
import com.getyourguide.openapi.validation.api.selector.TrafficSelector;
67
import com.getyourguide.openapi.validation.core.OpenApiRequestValidator;
78
import com.getyourguide.openapi.validation.factory.ReactiveMetaDataFactory;
89
import com.getyourguide.openapi.validation.filter.decorator.BodyCachingServerHttpRequestDecorator;
910
import com.getyourguide.openapi.validation.filter.decorator.BodyCachingServerHttpResponseDecorator;
1011
import com.getyourguide.openapi.validation.filter.decorator.DecoratorBuilder;
12+
import javax.annotation.Nullable;
1113
import lombok.AllArgsConstructor;
1214
import org.springframework.core.Ordered;
1315
import org.springframework.core.annotation.Order;
@@ -59,18 +61,21 @@ private Mono<Void> decorateWithValidation(ServerWebExchange exchange, WebFilterC
5961
// Note: Errors are not handled here. They could be handled with SignalType.ON_ERROR, but then the response body can't be accessed.
6062
// Reason seems to be that those don't use the decorated response that is set here, but use the previous response object.
6163
if (signalType == SignalType.ON_COMPLETE) {
64+
var responseMetaData = metaDataFactory.buildResponseMetaData(responseDecorated);
6265
if (!alreadyDidRequestValidation) {
63-
validateRequest(requestDecorated, requestMetaData, RunType.ASYNC);
66+
validateRequest(requestDecorated, requestMetaData, responseMetaData, RunType.ASYNC);
6467
}
6568
if (!alreadyDidResponseValidation) {
66-
validateResponse(responseDecorated, requestMetaData, RunType.ASYNC);
69+
validateResponse(
70+
requestMetaData, responseMetaData, responseDecorated.getCachedBody(), RunType.ASYNC);
6771
}
6872
}
6973
});
7074
}
7175

7276
/**
7377
* Validate request and fail on violation if configured to do so.
78+
*
7479
* @return true if validation is done as part of this method
7580
*/
7681
private boolean validateRequestWithFailOnViolation(
@@ -83,11 +88,11 @@ private boolean validateRequestWithFailOnViolation(
8388

8489
if (requestDecorated.getHeaders().containsKey("Content-Type") && requestDecorated.getHeaders().containsKey("Content-Length")) {
8590
requestDecorated.setOnBodyCachedListener(() -> {
86-
var validateRequestResult = validateRequest(requestDecorated, requestMetaData, RunType.SYNC);
91+
var validateRequestResult = validateRequest(requestDecorated, requestMetaData, null, RunType.SYNC);
8792
throwStatusExceptionOnViolation(validateRequestResult, "Request validation failed");
8893
});
8994
} else {
90-
var validateRequestResult = validateRequest(requestDecorated, requestMetaData, RunType.SYNC);
95+
var validateRequestResult = validateRequest(requestDecorated, requestMetaData, null, RunType.SYNC);
9196
throwStatusExceptionOnViolation(validateRequestResult, "Request validation failed");
9297
}
9398
return true;
@@ -101,6 +106,7 @@ private static void throwStatusExceptionOnViolation(ValidationResult validateReq
101106

102107
/**
103108
* Validate response and fail on violation if configured to do so.
109+
*
104110
* @return true if validation is done as part of this method
105111
*/
106112
private boolean validateResponseWithFailOnViolation(
@@ -112,7 +118,12 @@ private boolean validateResponseWithFailOnViolation(
112118
}
113119

114120
responseDecorated.setOnBodyCachedListener(() -> {
115-
var validateResponseResult = validateResponse(responseDecorated, requestMetaData, RunType.SYNC);
121+
var validateResponseResult = validateResponse(
122+
requestMetaData,
123+
metaDataFactory.buildResponseMetaData(responseDecorated),
124+
responseDecorated.getCachedBody(),
125+
RunType.SYNC
126+
);
116127
throwStatusExceptionOnViolation(validateResponseResult, "Response validation failed");
117128
});
118129
return true;
@@ -121,37 +132,38 @@ private boolean validateResponseWithFailOnViolation(
121132
private ValidationResult validateRequest(
122133
BodyCachingServerHttpRequestDecorator request,
123134
RequestMetaData requestMetaData,
135+
@Nullable ResponseMetaData responseMetaData,
124136
RunType runType
125137
) {
126138
if (!trafficSelector.canRequestBeValidated(requestMetaData)) {
127139
return ValidationResult.NOT_APPLICABLE;
128140
}
129141

130142
if (runType == RunType.SYNC) {
131-
return validator.validateRequestObject(requestMetaData, request.getCachedBody());
143+
return validator.validateRequestObject(requestMetaData, responseMetaData, request.getCachedBody());
132144
} else {
133-
validator.validateRequestObjectAsync(requestMetaData, request.getCachedBody());
145+
validator.validateRequestObjectAsync(requestMetaData, responseMetaData, request.getCachedBody());
134146
return ValidationResult.NOT_APPLICABLE;
135147
}
136148
}
137149

138150
private ValidationResult validateResponse(
139-
BodyCachingServerHttpResponseDecorator response,
140151
RequestMetaData requestMetaData,
152+
@Nullable ResponseMetaData responseMetaData,
153+
@Nullable String responseBody,
141154
RunType runType
142155
) {
143-
var responseMetaData = metaDataFactory.buildResponseMetaData(response);
144156
if (!trafficSelector.canResponseBeValidated(requestMetaData, responseMetaData)) {
145157
return ValidationResult.NOT_APPLICABLE;
146158
}
147159

148160
if (runType == RunType.SYNC) {
149-
return validator.validateResponseObject(requestMetaData, responseMetaData, response.getCachedBody());
161+
return validator.validateResponseObject(requestMetaData, responseMetaData, responseBody);
150162
} else {
151-
validator.validateResponseObjectAsync(requestMetaData, responseMetaData, response.getCachedBody());
163+
validator.validateResponseObjectAsync(requestMetaData, responseMetaData, responseBody);
152164
return ValidationResult.NOT_APPLICABLE;
153165
}
154166
}
155167

156-
private enum RunType { SYNC, ASYNC }
168+
private enum RunType {SYNC, ASYNC}
157169
}

0 commit comments

Comments
 (0)