From 0eadc6e1ba68259d308a672ce4e0893f81433e80 Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 15 Jun 2026 14:56:39 +0800 Subject: [PATCH] ISSUE-46: fix notification SSE buffering Signed-off-by: dongmucat <1127093059@qq.com> --- .../skillhub/filter/RequestLoggingFilter.java | 17 ++++++ .../filter/RequestLoggingFilterTest.java | 54 ++++++++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestLoggingFilter.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestLoggingFilter.java index cda766ff8..b1e00c012 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestLoggingFilter.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/filter/RequestLoggingFilter.java @@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; @@ -40,6 +42,11 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse filterChain.doFilter(request, response); return; } + if (isNotificationSse(uri)) { + prepareSseResponse(response); + filterChain.doFilter(request, response); + return; + } ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(response); @@ -92,6 +99,16 @@ private boolean shouldSkip(String uri) { return false; } + private boolean isNotificationSse(String uri) { + return uri != null && uri.endsWith("/notifications/sse"); + } + + private void prepareSseResponse(HttpServletResponse response) { + response.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE); + response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-transform"); + response.setHeader("X-Accel-Buffering", "no"); + } + private String getRequestBody(ContentCachingRequestWrapper request) { byte[] buf = request.getContentAsByteArray(); if (buf.length > 0) { diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestLoggingFilterTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestLoggingFilterTest.java index c79708a3f..5b39ffeba 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestLoggingFilterTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/filter/RequestLoggingFilterTest.java @@ -1,22 +1,26 @@ package com.iflytek.skillhub.filter; +import static org.assertj.core.api.Assertions.assertThat; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.springframework.web.util.ContentCachingResponseWrapper; class RequestLoggingFilterTest { @@ -101,6 +105,44 @@ void doFilterInternal_logsCoreSummaryFields() assertThat(loggedMessages()).noneMatch(message -> message.contains("Headers: {")); } + @Test + void doFilterInternal_shouldBypassCachingWrapperForNotificationSse() throws Exception { + RequestLoggingFilter filter = new RequestLoggingFilter(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/web/notifications/sse"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicReference responseSeenByChain = new AtomicReference<>(); + FilterChain chain = (servletRequest, servletResponse) -> { + responseSeenByChain.set(servletResponse); + servletResponse.getWriter().write("event: connected\n"); + servletResponse.flushBuffer(); + }; + + filter.doFilter(request, response, chain); + + assertThat(responseSeenByChain.get()).isSameAs(response); + assertThat(response.getHeader("X-Accel-Buffering")).isEqualTo("no"); + assertThat(response.getHeader(HttpHeaders.CACHE_CONTROL)).isEqualTo("no-cache, no-transform"); + assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM_VALUE); + assertThat(response.getContentAsString()).contains("event: connected"); + } + + @Test + void doFilterInternal_shouldKeepCachingWrapperForRegularApiResponses() throws Exception { + RequestLoggingFilter filter = new RequestLoggingFilter(); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/web/notifications/unread-count"); + MockHttpServletResponse response = new MockHttpServletResponse(); + AtomicReference responseSeenByChain = new AtomicReference<>(); + FilterChain chain = (servletRequest, servletResponse) -> { + responseSeenByChain.set(servletResponse); + servletResponse.getWriter().write("{\"count\":1}"); + }; + + filter.doFilter(request, response, chain); + + assertThat(responseSeenByChain.get()).isInstanceOf(ContentCachingResponseWrapper.class); + assertThat(response.getContentAsString()).isEqualTo("{\"count\":1}"); + } + private void attachAppender() { logger.setLevel(Level.INFO); appender = new ListAppender<>();