Skip to content

Commit ef939a0

Browse files
GH-3283: HTTP Inbound handle SpEL errors (#3289)
* GH-3283: HTTP Inbound handle SpEL errors Fixes #3283 * Process all the request message preparation exceptions in the provided error channel to let target application to make a decision about an appropriate HTTP status instead of default 500 Server Error * * Rephrase `ResponseStatusException` doc in the http.adoc Co-authored-by: Gary Russell <[email protected]> Co-authored-by: Gary Russell <[email protected]>
1 parent 5bd6278 commit ef939a0

File tree

3 files changed

+67
-17
lines changed

3 files changed

+67
-17
lines changed

spring-integration-http/src/main/java/org/springframework/integration/http/inbound/HttpRequestHandlingEndpointSupport.java

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@
5959
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
6060
import org.springframework.integration.support.json.JacksonPresent;
6161
import org.springframework.messaging.Message;
62+
import org.springframework.messaging.MessageChannel;
6263
import org.springframework.messaging.MessageHeaders;
6364
import org.springframework.messaging.MessagingException;
65+
import org.springframework.messaging.converter.MessageConversionException;
6466
import org.springframework.util.Assert;
6567
import org.springframework.util.CollectionUtils;
6668
import org.springframework.util.LinkedMultiValueMap;
@@ -267,30 +269,46 @@ private Message<?> actualDoHandleRequest(HttpServletRequest servletRequest, Requ
267269

268270
Map<String, Object> headers = getHeaderMapper().toHeaders(httpEntity.getHeaders());
269271
Object payload = null;
270-
if (getPayloadExpression() != null) {
271-
// create payload based on SpEL
272-
payload = getPayloadExpression().getValue(evaluationContext);
273-
}
272+
Message<?> message = null;
273+
try {
274+
if (getPayloadExpression() != null) {
275+
// create payload based on SpEL
276+
payload = getPayloadExpression().getValue(evaluationContext);
277+
}
274278

275-
if (!CollectionUtils.isEmpty(getHeaderExpressions())) {
276-
headers.putAll(
277-
ExpressionEvalMap.from(getHeaderExpressions())
278-
.usingEvaluationContext(evaluationContext)
279-
.withRoot(httpEntity)
280-
.build());
281-
}
279+
if (!CollectionUtils.isEmpty(getHeaderExpressions())) {
280+
headers.putAll(
281+
ExpressionEvalMap.from(getHeaderExpressions())
282+
.usingEvaluationContext(evaluationContext)
283+
.withRoot(httpEntity)
284+
.build());
285+
}
282286

283-
if (payload == null) {
284-
if (httpEntity.getBody() != null) {
285-
payload = httpEntity.getBody();
287+
if (payload == null) {
288+
if (httpEntity.getBody() != null) {
289+
payload = httpEntity.getBody();
290+
}
291+
else {
292+
payload = requestParams;
293+
}
294+
}
295+
296+
message = prepareRequestMessage(servletRequest, httpEntity, headers, payload);
297+
}
298+
catch (Exception ex) {
299+
MessageConversionException conversionException =
300+
new MessageConversionException("Cannot create request message", ex);
301+
MessageChannel errorChannel = getErrorChannel();
302+
if (errorChannel != null) {
303+
this.messagingTemplate.send(errorChannel,
304+
buildErrorMessage(null,
305+
conversionException));
286306
}
287307
else {
288-
payload = requestParams;
308+
throw conversionException;
289309
}
290310
}
291311

292-
Message<?> message = prepareRequestMessage(servletRequest, httpEntity, headers, payload);
293-
294312
Message<?> reply = null;
295313
if (isExpectReply()) {
296314
try {

spring-integration-http/src/test/java/org/springframework/integration/http/dsl/HttpDslTests.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.context.annotation.Bean;
4141
import org.springframework.context.annotation.Configuration;
4242
import org.springframework.http.HttpMethod;
43+
import org.springframework.http.HttpStatus;
4344
import org.springframework.http.MediaType;
4445
import org.springframework.http.ResponseEntity;
4546
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -56,6 +57,7 @@
5657
import org.springframework.messaging.Message;
5758
import org.springframework.messaging.MessageChannel;
5859
import org.springframework.messaging.PollableChannel;
60+
import org.springframework.messaging.support.ErrorMessage;
5961
import org.springframework.mock.web.MockPart;
6062
import org.springframework.security.access.AccessDecisionManager;
6163
import org.springframework.security.access.vote.AffirmativeBased;
@@ -81,6 +83,7 @@
8183
import org.springframework.web.context.WebApplicationContext;
8284
import org.springframework.web.multipart.MultipartResolver;
8385
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
86+
import org.springframework.web.server.ResponseStatusException;
8487
import org.springframework.web.servlet.DispatcherServlet;
8588

8689
/**
@@ -235,6 +238,30 @@ public void testValidation() throws Exception {
235238
flowRegistration.destroy();
236239
}
237240

241+
@Test
242+
public void testBadRequest() throws Exception {
243+
IntegrationFlow flow =
244+
IntegrationFlows.from(
245+
Http.inboundGateway("/badRequest")
246+
.errorChannel((message, timeout) -> {
247+
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
248+
"Not valid request param", ((ErrorMessage) message).getPayload());
249+
})
250+
.payloadExpression("#requestParams.p1"))
251+
.get();
252+
253+
IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
254+
this.integrationFlowContext.registration(flow).register();
255+
256+
this.mockMvc.perform(
257+
get("/badRequest")
258+
.with(httpBasic("user", "user"))
259+
.param("p2", "P2"))
260+
.andExpect(status().isBadRequest())
261+
.andExpect(status().reason("Not valid request param"));
262+
263+
flowRegistration.destroy();
264+
}
238265

239266
@Configuration
240267
@EnableWebSecurity

src/reference/asciidoc/http.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,11 @@ If the error flow times out after a main flow timeout, `500 Internal Server Erro
431431
NOTE: Previously, the default status code for a timeout was `200 OK`.
432432
To restore that behavior, set `reply-timeout-status-code-expression="200"`.
433433

434+
Also starting with version 5.4, an error that is encountered while preparing the request message is sent to the error channel (if provided).
435+
A decision about throwing an appropriate exception should be done in the error flow by examining the exception.
436+
Previously, any exceptions were simply thrown, causing an HTTP 500 server error response status, but in some cases the problem can be caused by incorrect request params, so a `ResponseStatusException` with a 4xx client error status should be thrown instead.
437+
See `ResponseStatusException` for more information.
438+
The `ErrorMessage` sent to this error channel contains the original exception as the payload for analysis.
434439
==== URI Template Variables and Expressions
435440

436441
By using the `path` attribute in conjunction with the `payload-expression` attribute and the `header` element, you have a high degree of flexibility for mapping inbound request data.

0 commit comments

Comments
 (0)