Skip to content

Commit 975f31c

Browse files
committed
fix: Avoid HttpMessageNotWritableException when an exception happens in an SSE stream
Fixes #126
1 parent 7277a82 commit 975f31c

File tree

3 files changed

+40
-2
lines changed

3 files changed

+40
-2
lines changed

src/main/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/servlet/ErrorHandlingControllerAdvice.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.slf4j.Logger;
66
import org.slf4j.LoggerFactory;
77
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
8+
import org.springframework.http.MediaType;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.annotation.ControllerAdvice;
1011
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -33,6 +34,7 @@ public ResponseEntity<?> handleException(Throwable exception, WebRequest webRequ
3334
ApiErrorResponse errorResponse = errorHandlingFacade.handle(exception);
3435

3536
return ResponseEntity.status(Objects.requireNonNull(errorResponse.getHttpStatus()))
37+
.contentType(MediaType.APPLICATION_JSON)
3638
.body(errorResponse);
3739
}
3840
}

src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/IntegrationTest.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@
22

33
import io.github.wimdeblauwe.errorhandlingspringbootstarter.exception.MyCustomHttpResponseStatusException;
44
import io.github.wimdeblauwe.errorhandlingspringbootstarter.mapper.HttpResponseStatusFromExceptionMapper;
5+
import org.hamcrest.Matchers;
56
import org.junit.jupiter.api.Test;
67
import org.springframework.beans.factory.annotation.Autowired;
78
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
89
import org.springframework.context.annotation.Bean;
910
import org.springframework.context.annotation.Import;
1011
import org.springframework.http.HttpStatusCode;
12+
import org.springframework.http.MediaType;
1113
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
1214
import org.springframework.security.web.SecurityFilterChain;
1315
import org.springframework.test.web.servlet.MockMvc;
16+
import org.springframework.test.web.servlet.MvcResult;
1417

1518
import java.time.Instant;
1619

20+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
1721
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
1822
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
19-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
23+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
2124

2225
@WebMvcTest(value = IntegrationTestRestController.class,
2326
properties = {"spring.main.allow-bean-definition-overriding=true",
@@ -58,6 +61,18 @@ void testExceptionWithCustomStatus() throws Exception {
5861
;
5962
}
6063

64+
@Test
65+
void testSse() throws Exception {
66+
MvcResult result = mockMvc.perform(get("/integration-test/sse")
67+
.accept(MediaType.TEXT_EVENT_STREAM))
68+
.andExpect(request().asyncStarted())
69+
.andReturn();
70+
71+
mockMvc.perform(asyncDispatch(result))
72+
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
73+
.andExpect(content().string(Matchers.containsString("Simulated SSE error")));
74+
}
75+
6176
static class WebSecurityConfig {
6277
@Bean
6378
public SecurityFilterChain securityFilterChain(HttpSecurity http) {

src/test/java/io/github/wimdeblauwe/errorhandlingspringbootstarter/IntegrationTestRestController.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import org.springframework.web.bind.annotation.GetMapping;
77
import org.springframework.web.bind.annotation.RequestMapping;
88
import org.springframework.web.bind.annotation.RestController;
9+
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
10+
11+
import java.io.IOException;
912

1013
@RestController
1114
@RequestMapping("/integration-test")
@@ -25,4 +28,22 @@ void throwExceptionWithBadRequestStatus() {
2528
void throwMyCustomHttpResponseStatusException() {
2629
throw new MyCustomHttpResponseStatusException(HttpStatus.I_AM_A_TEAPOT);
2730
}
31+
32+
@GetMapping(value = "sse", produces = "text/event-stream")
33+
SseEmitter sseEndpoint() throws IOException {
34+
SseEmitter emitter = new SseEmitter();
35+
emitter.send("test");
36+
37+
// Simulate error during stream processing
38+
new Thread(() -> {
39+
try {
40+
Thread.sleep(10);
41+
emitter.completeWithError(new RuntimeException("Simulated SSE error"));
42+
} catch (InterruptedException e) {
43+
Thread.currentThread().interrupt();
44+
}
45+
}).start();
46+
47+
return emitter;
48+
}
2849
}

0 commit comments

Comments
 (0)