This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 261db07cae fix(logging): apply plugin body size limits to
request/response logging (#7852)
261db07cae is described below
commit 261db07cae710abc71ad7de8f72280757d54cd4e
Author: Duansg <[email protected]>
AuthorDate: Mon Oct 19 21:11:53 2026 -0700
fix(logging): apply plugin body size limits to request/response logging
(#7852)
---
.../logging/common/AbstractLoggingPlugin.java | 6 +-
.../plugin/logging/common/body/BodyWriter.java | 124 ++++--
.../common/body/LoggingServerHttpRequest.java | 23 +-
.../common/body/LoggingServerHttpResponse.java | 32 +-
.../logging/common/config/GenericGlobalConfig.java | 14 +-
.../common/utils/LogCollectConfigUtils.java | 35 +-
.../common/AbstractLoggingPluginBodyLimitTest.java | 467 +++++++++++++++++++++
.../plugin/logging/common/body/BodyWriterTest.java | 172 ++++++++
.../common/body/LoggingBodySaturatedSizeTest.java | 94 +++++
.../common/utils/LogCollectConfigUtilsTest.java | 46 ++
10 files changed, 943 insertions(+), 70 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPlugin.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPlugin.java
index 3c9ad03be7..79166a9dc6 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPlugin.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPlugin.java
@@ -28,6 +28,7 @@ import org.apache.shenyu.plugin.base.utils.HostAddressUtils;
import org.apache.shenyu.plugin.logging.common.body.LoggingServerHttpRequest;
import org.apache.shenyu.plugin.logging.common.body.LoggingServerHttpResponse;
import org.apache.shenyu.plugin.logging.common.collector.LogCollector;
+import org.apache.shenyu.plugin.logging.common.config.GenericGlobalConfig;
import org.apache.shenyu.plugin.logging.common.constant.GenericLoggingConstant;
import org.apache.shenyu.plugin.logging.common.entity.CommonLoggingRuleHandle;
import org.apache.shenyu.plugin.logging.common.entity.ShenyuRequestLog;
@@ -115,9 +116,10 @@ public abstract class AbstractLoggingPlugin<L extends
ShenyuRequestLog> extends
requestInfo.setSelectorId(selector.getId());
requestInfo.setRuleId(rule.getId());
requestInfo.setNamespaceId(rule.getNamespaceId());
- LoggingServerHttpRequest<L> loggingServerHttpRequest = new
LoggingServerHttpRequest<>(request, requestInfo);
+ GenericGlobalConfig globalConfig =
LogCollectConfigUtils.getGlobalConfig(selector.getPluginId());
+ LoggingServerHttpRequest<L> loggingServerHttpRequest = new
LoggingServerHttpRequest<>(request, requestInfo,
globalConfig.getMaxRequestBody());
LoggingServerHttpResponse<L> loggingServerHttpResponse = new
LoggingServerHttpResponse<>(exchange.getResponse(),
- requestInfo, this.logCollector(), desensitized, keyWordMatch,
dataDesensitizeAlg);
+ requestInfo, this.logCollector(), desensitized, keyWordMatch,
dataDesensitizeAlg, globalConfig.getMaxResponseBody());
ServerWebExchange webExchange =
exchange.mutate().request(loggingServerHttpRequest)
.response(loggingServerHttpResponse).build();
loggingServerHttpResponse.setExchange(webExchange);
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/BodyWriter.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/BodyWriter.java
index 5a63755138..68832aec5a 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/BodyWriter.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/BodyWriter.java
@@ -26,82 +26,122 @@ import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.Objects;
/**
* bodyWriter is used to read Body.
+ *
+ * <p>The body is buffered only up to {@code maxBodySize} bytes. Once the
limit is exceeded the
+ * buffer is released (so it can be garbage collected before the request
completes) and only the
+ * total size keeps being tracked. All methods are
+ * synchronized because reactive signals (e.g. cancel-triggered {@code
doFinally}) are not
+ * guaranteed to be serialized with {@code onNext}.</p>
*/
public class BodyWriter {
private static final Logger LOG =
LoggerFactory.getLogger(BodyWriter.class);
- private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ private final int maxBodySize;
+
+ /**
+ * Lazily allocated on the first buffered write and released on overflow;
guarded by {@code this}.
+ */
+ private ByteArrayOutputStream stream;
- private final WritableByteChannel channel = Channels.newChannel(stream);
+ private WritableByteChannel channel;
- private final AtomicBoolean isClosed = new AtomicBoolean(false);
+ private long size;
+ private boolean overflow;
+
+ private boolean closed;
+
+ /**
+ * Create a writer without a body limit (legacy behavior).
+ */
+ public BodyWriter() {
+ this(Integer.MAX_VALUE);
+ }
+
+ /**
+ * Create a writer that stops buffering once the body exceeds the limit.
+ *
+ * @param maxBodySize maximum buffered body size in bytes; non-positive
values buffer nothing
+ */
+ public BodyWriter(final int maxBodySize) {
+ this.maxBodySize = Math.max(0, maxBodySize);
+ }
/**
* write ByteBuffer.
*
* @param buffer byte buffer
*/
- public void write(final ByteBuffer buffer) {
- if (!isClosed.get()) {
- try {
- channel.write(buffer);
- } catch (IOException e) {
- isClosed.compareAndSet(false, true);
- LOG.error("write buffer Failed.", e);
- }
+ public synchronized void write(final ByteBuffer buffer) {
+ if (closed || !buffer.hasRemaining()) {
+ return;
+ }
+ size += buffer.remaining();
+ if (overflow) {
+ return;
+ }
+ if (size > maxBodySize) {
+ // release the buffered prefix so its array can be collected
before the request ends;
+ // only the total size is tracked from now on
+ overflow = true;
+ stream = null;
+ channel = null;
+ return;
+ }
+ if (Objects.isNull(stream)) {
+ stream = new ByteArrayOutputStream();
+ channel = Channels.newChannel(stream);
+ }
+ try {
+ channel.write(buffer);
+ } catch (IOException e) {
+ closed = true;
+ LOG.error("write buffer Failed.", e);
}
}
/**
- * judge stream is empty.
+ * Judge whether no body bytes have been observed.
*
- * @return true: stream is empty
+ * @return true when the body is empty
*/
- public boolean isEmpty() {
- return stream.size() == 0;
+ public synchronized boolean isEmpty() {
+ return size == 0;
}
/**
- * get stream size.
+ * Get the observed body size, including bytes discarded after exceeding
the limit.
*
- * @return size of stream
+ * @return observed byte count, capped at {@link Integer#MAX_VALUE}
*/
- public int size() {
- return stream.size();
+ public synchronized int size() {
+ return (int) Math.min(size, Integer.MAX_VALUE);
}
/**
- * output stream value.
+ * Judge whether the body exceeded the limit and was discarded.
*
- * @return string of stream
+ * @return true when the body has been discarded
*/
- public String output() {
- if (isEmpty()) {
+ public synchronized boolean isLimitExceeded() {
+ return overflow;
+ }
+
+ /**
+ * Output the buffered body and stop accepting further writes.
+ *
+ * @return string of stream, or an empty string if the body is empty or
exceeds the limit
+ */
+ public synchronized String output() {
+ closed = true;
+ if (size == 0 || overflow || Objects.isNull(stream)) {
return "";
}
- try {
- isClosed.compareAndSet(false, true);
- return new String(stream.toByteArray(), StandardCharsets.UTF_8);
- } catch (Exception e) {
- LOG.error("Write failed: ", e);
- return "Write failed: " + e.getMessage();
- } finally {
- try {
- stream.close();
- } catch (IOException e) {
- LOG.error("Close stream error: ", e);
- }
- try {
- channel.close();
- } catch (IOException e) {
- LOG.error("Close channel error: ", e);
- }
- }
+ return new String(stream.toByteArray(), StandardCharsets.UTF_8);
}
}
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpRequest.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpRequest.java
index b0b0fda8c7..58a9d3aa22 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpRequest.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpRequest.java
@@ -17,8 +17,8 @@
package org.apache.shenyu.plugin.logging.common.body;
+import org.apache.shenyu.plugin.logging.common.config.GenericGlobalConfig;
import org.apache.shenyu.plugin.logging.common.entity.ShenyuRequestLog;
-import org.apache.shenyu.plugin.logging.common.utils.LogCollectConfigUtils;
import org.apache.shenyu.plugin.logging.common.utils.LogCollectUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -33,9 +33,23 @@ public class LoggingServerHttpRequest<L extends
ShenyuRequestLog> extends Server
private final L logInfo;
+ private final int maxRequestBody;
+
public LoggingServerHttpRequest(final ServerHttpRequest delegate, final L
logInfo) {
+ this(delegate, logInfo, GenericGlobalConfig.DEFAULT_MAX_REQUEST_BODY);
+ }
+
+ /**
+ * Create a request decorator using the body limit captured for this
request.
+ *
+ * @param delegate delegate request
+ * @param logInfo access log
+ * @param maxRequestBody maximum logged request body size in bytes
+ */
+ public LoggingServerHttpRequest(final ServerHttpRequest delegate, final L
logInfo, final int maxRequestBody) {
super(delegate);
this.logInfo = logInfo;
+ this.maxRequestBody = maxRequestBody;
}
/**
@@ -46,7 +60,7 @@ public class LoggingServerHttpRequest<L extends
ShenyuRequestLog> extends Server
@Override
@NonNull
public Flux<DataBuffer> getBody() {
- BodyWriter writer = new BodyWriter();
+ BodyWriter writer = new BodyWriter(maxRequestBody);
return super.getBody().doOnNext(dataBuffer -> {
if (LogCollectUtils.isNotBinaryType(getHeaders())) {
try (DataBuffer.ByteBufferIterator bufferIterator =
dataBuffer.readableByteBuffers()) {
@@ -55,9 +69,8 @@ public class LoggingServerHttpRequest<L extends
ShenyuRequestLog> extends Server
}
}).doFinally(signal -> {
- int size = writer.size();
- boolean requestBodyTooLarge =
LogCollectConfigUtils.isRequestBodyTooLarge(size);
- if (size == 0 || requestBodyTooLarge) {
+ // use the writer's overflow state: size() saturates at
Integer.MAX_VALUE
+ if (writer.isEmpty() || writer.isLimitExceeded()) {
return;
}
String body = writer.output();
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpResponse.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpResponse.java
index 506cb6551d..97ce38c87e 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpResponse.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/body/LoggingServerHttpResponse.java
@@ -26,9 +26,9 @@ import org.apache.shenyu.plugin.api.result.ShenyuResult;
import org.apache.shenyu.plugin.api.result.ShenyuResultWrap;
import org.apache.shenyu.plugin.base.utils.MediaTypeUtils;
import org.apache.shenyu.plugin.logging.common.collector.LogCollector;
+import org.apache.shenyu.plugin.logging.common.config.GenericGlobalConfig;
import org.apache.shenyu.plugin.logging.common.constant.GenericLoggingConstant;
import org.apache.shenyu.plugin.logging.common.entity.ShenyuRequestLog;
-import org.apache.shenyu.plugin.logging.common.utils.LogCollectConfigUtils;
import org.apache.shenyu.plugin.logging.common.utils.LogCollectUtils;
import org.apache.shenyu.plugin.logging.desensitize.api.matcher.KeyWordMatch;
import org.reactivestreams.Publisher;
@@ -76,6 +76,8 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
private final KeyWordMatch keyWordMatch;
+ private final int maxResponseBody;
+
/**
* Constructor LoggingServerHttpResponse.
*
@@ -105,12 +107,30 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
public LoggingServerHttpResponse(final ServerHttpResponse delegate, final
L logInfo,
final LogCollector<L> logCollector, final
boolean desensitized,
final KeyWordMatch keyWordMatch, final
String dataDesensitizeAlg) {
+ this(delegate, logInfo, logCollector, desensitized, keyWordMatch,
dataDesensitizeAlg, GenericGlobalConfig.DEFAULT_MAX_RESPONSE_BODY);
+ }
+
+ /**
+ * Create a response decorator using the body limit captured for this
request.
+ *
+ * @param delegate delegate response
+ * @param logInfo access log
+ * @param logCollector LogCollector instance
+ * @param desensitized desensitize flag
+ * @param keyWordMatch compiled keyword matcher
+ * @param dataDesensitizeAlg desensitize function
+ * @param maxResponseBody maximum logged response body size in bytes
+ */
+ public LoggingServerHttpResponse(final ServerHttpResponse delegate, final
L logInfo,
+ final LogCollector<L> logCollector, final
boolean desensitized,
+ final KeyWordMatch keyWordMatch, final
String dataDesensitizeAlg, final int maxResponseBody) {
super(delegate);
this.logInfo = logInfo;
this.logCollector = logCollector;
this.desensitized = desensitized;
this.dataDesensitizeAlg = dataDesensitizeAlg;
this.keyWordMatch = keyWordMatch;
+ this.maxResponseBody = maxResponseBody;
}
/**
@@ -153,7 +173,7 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
if (MediaTypeUtils.isByteType(mediaType)) {
return Flux.from(body).doFinally(signal ->
logResponse(shenyuContext, null));
}
- BodyWriter writer = new BodyWriter();
+ BodyWriter writer = new BodyWriter(maxResponseBody);
return Flux.from(body).doOnNext(buffer -> {
if (LogCollectUtils.isNotBinaryType(getHeaders())) {
try (DataBuffer.ByteBufferIterator bufferIterator =
buffer.readableByteBuffers()) {
@@ -186,8 +206,8 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
logInfo.setUpstreamIp(getUpstreamIp());
}
if (Objects.nonNull(writer)) {
- int size = writer.size();
- if (size > 0 &&
!LogCollectConfigUtils.isResponseBodyTooLarge(size)) {
+ // use the writer's overflow state: size() saturates at
Integer.MAX_VALUE
+ if (!writer.isEmpty() && !writer.isLimitExceeded()) {
String body = writer.output();
logInfo.setResponseBody(body);
}
@@ -243,7 +263,7 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
/**
* collect access error.
*
- * @param throwable Exception occurred。
+ * @param throwable Exception occurred.
*/
public void logError(final Throwable throwable) {
HttpStatusCode httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
@@ -282,7 +302,7 @@ public class LoggingServerHttpResponse<L extends
ShenyuRequestLog> extends Serve
}
int size = bytes.length;
- if (size > 0 && !LogCollectConfigUtils.isResponseBodyTooLarge(size)) {
+ if (size > 0 && size <= maxResponseBody) {
String body = new String(bytes, StandardCharsets.UTF_8);
logInfo.setResponseBody(body);
}
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/config/GenericGlobalConfig.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/config/GenericGlobalConfig.java
index 7bced39ce6..b651d25715 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/config/GenericGlobalConfig.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/config/GenericGlobalConfig.java
@@ -24,6 +24,16 @@ import
org.apache.shenyu.plugin.logging.common.sampler.Sampler;
*/
public class GenericGlobalConfig {
+ /**
+ * Default response body limit in bytes (512 KiB).
+ */
+ public static final int DEFAULT_MAX_RESPONSE_BODY = 524288;
+
+ /**
+ * Default request body limit in bytes (512 KiB).
+ */
+ public static final int DEFAULT_MAX_REQUEST_BODY = 524288;
+
private String sampleRate = "1";
private Sampler sampler;
@@ -31,12 +41,12 @@ public class GenericGlobalConfig {
/**
* default 512KB.
*/
- private int maxResponseBody = 524288;
+ private int maxResponseBody = DEFAULT_MAX_RESPONSE_BODY;
/**
* default 512kb.
*/
- private int maxRequestBody = 524288;
+ private int maxRequestBody = DEFAULT_MAX_REQUEST_BODY;
/**
* default 50000.
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtils.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtils.java
index 43c4fd20e1..6b94ba04ce 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtils.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/main/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtils.java
@@ -26,7 +26,6 @@ import
org.apache.shenyu.plugin.logging.common.sampler.CountSampler;
import org.apache.shenyu.plugin.logging.common.sampler.Sampler;
import org.springframework.web.server.ServerWebExchange;
-import java.util.Objects;
import java.util.Optional;
/**
@@ -34,8 +33,6 @@ import java.util.Optional;
*/
public final class LogCollectConfigUtils {
- private static GenericGlobalConfig genericGlobalConfig = new
GenericGlobalConfig();
-
private static Sampler globalSampler = Sampler.ALWAYS_SAMPLE;
private LogCollectConfigUtils() {
@@ -71,29 +68,41 @@ public final class LogCollectConfigUtils {
}
/**
- * judge whether request body too large.
+ * Get the current plugin configuration, falling back to the default
limits when absent.
+ *
+ * @param pluginId plugin id
+ * @return plugin configuration or a new default configuration
+ */
+ public static GenericGlobalConfig getGlobalConfig(final String pluginId) {
+ return Optional.ofNullable(pluginId)
+
.map(AbstractLogPluginDataHandler.getPluginGlobalConfigMap()::get)
+ .orElseGet(GenericGlobalConfig::new);
+ }
+
+ /**
+ * Judge whether the request body exceeds the default limit.
*
* @param bodySize body size
* @return whether request body too large
+ * @deprecated This method only checks the default limit. Use {@link
#getGlobalConfig(String)}
+ * and the plugin's configured request body limit instead.
*/
+ @Deprecated
public static boolean isRequestBodyTooLarge(final int bodySize) {
- if (Objects.isNull(genericGlobalConfig)) {
- return false;
- }
- return bodySize > genericGlobalConfig.getMaxRequestBody();
+ return bodySize > GenericGlobalConfig.DEFAULT_MAX_REQUEST_BODY;
}
/**
- * judge whether response body too large.
+ * Judge whether the response body exceeds the default limit.
*
* @param bodySize body size.
* @return whether response body too large
+ * @deprecated This method only checks the default limit. Use {@link
#getGlobalConfig(String)}
+ * and the plugin's configured response body limit instead.
*/
+ @Deprecated
public static boolean isResponseBodyTooLarge(final int bodySize) {
- if (Objects.isNull(genericGlobalConfig)) {
- return false;
- }
- return bodySize > genericGlobalConfig.getMaxResponseBody();
+ return bodySize > GenericGlobalConfig.DEFAULT_MAX_RESPONSE_BODY;
}
/**
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPluginBodyLimitTest.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPluginBodyLimitTest.java
new file mode 100644
index 0000000000..188d28f42b
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/AbstractLoggingPluginBodyLimitTest.java
@@ -0,0 +1,467 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.logging.common;
+
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.plugin.api.RemoteAddressResolver;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
+import org.apache.shenyu.plugin.api.result.DefaultShenyuResult;
+import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
+import org.apache.shenyu.plugin.logging.common.body.LoggingServerHttpRequest;
+import org.apache.shenyu.plugin.logging.common.body.LoggingServerHttpResponse;
+import org.apache.shenyu.plugin.logging.common.collector.LogCollector;
+import org.apache.shenyu.plugin.logging.common.config.GenericApiConfig;
+import org.apache.shenyu.plugin.logging.common.config.GenericGlobalConfig;
+import org.apache.shenyu.plugin.logging.common.entity.ShenyuRequestLog;
+import
org.apache.shenyu.plugin.logging.common.handler.AbstractLogPluginDataHandler;
+import org.apache.shenyu.plugin.logging.desensitize.api.matcher.KeyWordMatch;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests body limits through the logging plugin and its request and response
decorators.
+ */
+public final class AbstractLoggingPluginBodyLimitTest {
+
+ private static final String PLUGIN_ID = "body-limit-test";
+
+ private static final String OTHER_PLUGIN_ID = "other-body-limit-test";
+
+ private final RecordingLogCollector collector = new
RecordingLogCollector();
+
+ private final TestLoggingPlugin plugin = new TestLoggingPlugin(collector);
+
+ private final TestLogPluginDataHandler dataHandler = new
TestLogPluginDataHandler(collector);
+
+ private ApplicationContext previousContext;
+
+ private GenericApplicationContext applicationContext;
+
+ @BeforeEach
+ public void setUp() {
+ previousContext =
SpringBeanUtils.getInstance().getApplicationContext();
+ applicationContext = new GenericApplicationContext();
+ applicationContext.registerBean(RemoteAddressResolver.class, () -> new
RemoteAddressResolver() {
+ });
+ applicationContext.registerBean(DefaultShenyuResult.class);
+ applicationContext.refresh();
+
SpringBeanUtils.getInstance().setApplicationContext(applicationContext);
+ }
+
+ @AfterEach
+ public void tearDown() {
+
AbstractLogPluginDataHandler.getPluginGlobalConfigMap().remove(PLUGIN_ID);
+
AbstractLogPluginDataHandler.getPluginGlobalConfigMap().remove(OTHER_PLUGIN_ID);
+ SpringBeanUtils.getInstance().setApplicationContext(previousContext);
+ applicationContext.close();
+ }
+
+ @Test
+ public void testConfiguredRequestLimitAndResponseBoundary() {
+ updatePluginConfig(PLUGIN_ID, 4, 8);
+ ShenyuRequestLog log = execute(PLUGIN_ID, "hello", "response");
+ assertNull(log.getRequestBody());
+ assertEquals("response", log.getResponseBody());
+ }
+
+ @Test
+ public void testConfiguredResponseLimitAndRequestBoundary() {
+ updatePluginConfig(PLUGIN_ID, 8, 4);
+ ShenyuRequestLog log = execute(PLUGIN_ID, "request!", "hello");
+ assertEquals("request!", log.getRequestBody());
+ assertNull(log.getResponseBody());
+ }
+
+ @Test
+ public void testLimitsLargerThanDefault() {
+ updatePluginConfig(PLUGIN_ID, 1048576, 1048576);
+ String body = "x".repeat(524289);
+ ShenyuRequestLog log = execute(PLUGIN_ID, body, body);
+ assertTrue(body.equals(log.getRequestBody()), "Request body below the
configured limit should be logged");
+ assertTrue(body.equals(log.getResponseBody()), "Response body below
the configured limit should be logged");
+ }
+
+ @Test
+ public void testDefaultLimitsWhenConfigurationIsMissing() {
+ String body = "x".repeat(524288);
+ ShenyuRequestLog atLimit = execute(PLUGIN_ID, body, body);
+ assertEquals(body, atLimit.getRequestBody());
+ assertEquals(body, atLimit.getResponseBody());
+ ShenyuRequestLog overLimit = execute(PLUGIN_ID, body + "x", body +
"x");
+ assertNull(overLimit.getRequestBody());
+ assertNull(overLimit.getResponseBody());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"{}",
"{\"maxRequestBody\":null,\"maxResponseBody\":null}"})
+ public void testJsonDefaultsReplacePreviousLimits(final String configJson)
{
+ updatePluginConfig(PLUGIN_ID, 4, 4);
+ updatePluginConfig(PLUGIN_ID, configJson);
+ String body = "x".repeat(524288);
+ ShenyuRequestLog atLimit = execute(PLUGIN_ID, body, body);
+ assertEquals(body, atLimit.getRequestBody());
+ assertEquals(body, atLimit.getResponseBody());
+ ShenyuRequestLog overLimit = execute(PLUGIN_ID, body + "x", body +
"x");
+ assertNull(overLimit.getRequestBody());
+ assertNull(overLimit.getResponseBody());
+ }
+
+ @Test
+ public void testMissingJsonRequestLimitUsesDefault() {
+ updatePluginConfig(PLUGIN_ID, 4, 4);
+ updatePluginConfig(PLUGIN_ID, "{\"maxResponseBody\":4}");
+ String body = "x".repeat(524288);
+ ShenyuRequestLog atLimit = execute(PLUGIN_ID, body, "hello");
+ assertEquals(body, atLimit.getRequestBody());
+ assertNull(atLimit.getResponseBody());
+ assertNull(execute(PLUGIN_ID, body + "x", "hello").getRequestBody());
+ }
+
+ @Test
+ public void testMissingJsonResponseLimitUsesDefault() {
+ updatePluginConfig(PLUGIN_ID, 4, 4);
+ updatePluginConfig(PLUGIN_ID, "{\"maxRequestBody\":4}");
+ String body = "x".repeat(524288);
+ ShenyuRequestLog atLimit = execute(PLUGIN_ID, "hello", body);
+ assertNull(atLimit.getRequestBody());
+ assertEquals(body, atLimit.getResponseBody());
+ assertNull(execute(PLUGIN_ID, "hello", body + "x").getResponseBody());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {524288, 524289})
+ public void testLegacyRequestConstructorUsesDefaultLimit(final int
bodySize) {
+ String body = "x".repeat(bodySize);
+ ShenyuRequestLog log = new ShenyuRequestLog();
+ LoggingServerHttpRequest<ShenyuRequestLog> request = new
LoggingServerHttpRequest<>(createExchange(body).getRequest(), log);
+ request.getBody().doOnNext(DataBufferUtils::release).blockLast();
+ if (bodySize > 524288) {
+ assertNull(log.getRequestBody());
+ } else {
+ assertEquals(body, log.getRequestBody());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {524288, 524289})
+ public void testLegacyResponseConstructorUsesDefaultLimit(final int
bodySize) {
+ String body = "x".repeat(bodySize);
+ MockServerWebExchange exchange = createExchange("hello");
+ ShenyuRequestLog log = new ShenyuRequestLog();
+ LoggingServerHttpResponse<ShenyuRequestLog> response = new
LoggingServerHttpResponse<>(exchange.getResponse(),
+ log, collector, false, Collections.emptySet(), "md5");
+ response.setExchange(exchange);
+ response.writeWith(buffers(body)).block();
+ assertEquals(body, exchange.getResponse().getBodyAsString().block());
+ if (bodySize > 524288) {
+ assertNull(log.getResponseBody());
+ } else {
+ assertEquals(body, log.getResponseBody());
+ }
+ }
+
+ @Test
+ public void testPluginLimitsAreIsolated() {
+ updatePluginConfig(PLUGIN_ID, 4, 4);
+ updatePluginConfig(OTHER_PLUGIN_ID, 8, 8);
+ ShenyuRequestLog small = execute(PLUGIN_ID, "hello", "hello");
+ ShenyuRequestLog large = execute(OTHER_PLUGIN_ID, "hello", "hello");
+ final ShenyuRequestLog smallAgain = execute(PLUGIN_ID, "hello",
"hello");
+ assertNull(small.getRequestBody());
+ assertNull(small.getResponseBody());
+ assertEquals("hello", large.getRequestBody());
+ assertEquals("hello", large.getResponseBody());
+ assertNull(smallAgain.getRequestBody());
+ assertNull(smallAgain.getResponseBody());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testNestedPluginsKeepIndependentLimits(final boolean
smallerLimitFirst) {
+ updatePluginConfig(PLUGIN_ID, smallerLimitFirst ? 4 : 8,
smallerLimitFirst ? 4 : 8);
+ updatePluginConfig(OTHER_PLUGIN_ID, smallerLimitFirst ? 8 : 4,
smallerLimitFirst ? 8 : 4);
+ RecordingLogCollector otherCollector = new RecordingLogCollector();
+ TestLoggingPlugin otherPlugin = new TestLoggingPlugin(otherCollector);
+ MockServerWebExchange exchange = createExchange("hello");
+ AtomicReference<String> forwardedRequest = new AtomicReference<>();
+ plugin.doExecute(exchange, outer -> otherPlugin.doExecute(outer,
+ inner -> forward(inner, forwardedRequest, "hello"),
selector(OTHER_PLUGIN_ID), rule(OTHER_PLUGIN_ID)),
+ selector(PLUGIN_ID), rule(PLUGIN_ID)).block();
+ assertEquals("hello", forwardedRequest.get());
+ assertEquals("hello",
exchange.getResponse().getBodyAsString().block());
+ ShenyuRequestLog outerLog = collector.log.get();
+ assertNotNull(outerLog);
+ assertEquals(smallerLimitFirst ? null : "hello",
outerLog.getRequestBody());
+ assertEquals(smallerLimitFirst ? null : "hello",
outerLog.getResponseBody());
+ assertEquals(HttpStatus.CREATED.value(), outerLog.getStatus());
+ assertEquals(5, outerLog.getResponseContentLength());
+ ShenyuRequestLog innerLog = otherCollector.log.get();
+ assertNotNull(innerLog);
+ assertEquals(smallerLimitFirst ? "hello" : null,
innerLog.getRequestBody());
+ assertEquals(smallerLimitFirst ? "hello" : null,
innerLog.getResponseBody());
+ assertEquals(HttpStatus.CREATED.value(), innerLog.getStatus());
+ assertEquals(5, innerLog.getResponseContentLength());
+ }
+
+ @Test
+ public void testConfigUpdateAppliesToNextRequest() {
+ updatePluginConfig(PLUGIN_ID, 8, 8);
+ MockServerWebExchange exchange = createExchange("hello");
+ AtomicReference<String> forwardedRequest = new AtomicReference<>();
+ Mono<Void> inFlight = plugin.doExecute(exchange, decorated ->
forward(decorated, forwardedRequest, "hello"),
+ selector(PLUGIN_ID), rule(PLUGIN_ID));
+ updatePluginConfig(PLUGIN_ID, 4, 4);
+ inFlight.block();
+ assertEquals("hello", collector.log.get().getRequestBody());
+ assertEquals("hello", collector.log.get().getResponseBody());
+ ShenyuRequestLog nextRequest = execute(PLUGIN_ID, "hello", "hello");
+ assertNull(nextRequest.getRequestBody());
+ assertNull(nextRequest.getResponseBody());
+ updatePluginConfig(PLUGIN_ID, 8, 8);
+ ShenyuRequestLog updatedRequest = execute(PLUGIN_ID, "hello", "hello");
+ assertEquals("hello", updatedRequest.getRequestBody());
+ assertEquals("hello", updatedRequest.getResponseBody());
+ }
+
+ @Test
+ public void testUtf8BytesAreAccumulatedAcrossChunks() {
+ // two 3-byte UTF-8 characters, 6 bytes in total
+ final String first = Character.toString(0x20AC);
+ final String second = Character.toString(0x2122);
+ final String body = first + second;
+ updatePluginConfig(PLUGIN_ID, 5, 6);
+ MockServerWebExchange exchange = createExchange(first, second);
+ ShenyuRequestLog log = execute(PLUGIN_ID, exchange, body, first,
second);
+ assertNull(log.getRequestBody());
+ assertEquals(body, log.getResponseBody());
+ updatePluginConfig(PLUGIN_ID, 6, 5);
+ log = execute(PLUGIN_ID, createExchange(first, second), body, first,
second);
+ assertEquals(body, log.getRequestBody());
+ assertNull(log.getResponseBody());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, -1})
+ public void testNonPositiveLimitsOmitNonEmptyBodies(final int limit) {
+ updatePluginConfig(PLUGIN_ID, limit, limit);
+ ShenyuRequestLog log = execute(PLUGIN_ID, "hello", "hello");
+ assertNull(log.getRequestBody());
+ assertNull(log.getResponseBody());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ public void testErrorResponseUsesConfiguredLimit(final boolean
synchronous) {
+ updatePluginConfig(PLUGIN_ID, 8, 1);
+ ShenyuRequestLog small = executeError(synchronous);
+ assertEquals(500, small.getStatus());
+ assertTrue(small.getResponseContentLength() > 1);
+ assertNull(small.getResponseBody());
+ updatePluginConfig(PLUGIN_ID, 8, 1024);
+ ShenyuRequestLog large = executeError(synchronous);
+ assertEquals(500, large.getStatus());
+ assertNotNull(large.getResponseBody());
+ assertTrue(large.getResponseBody().contains("Internal Server Error"));
+ }
+
+ private ShenyuRequestLog executeError(final boolean synchronous) {
+ IllegalStateException failure = new IllegalStateException("failure");
+ assertSame(failure, assertThrows(IllegalStateException.class, () ->
plugin.doExecute(createExchange("hello"), exchange -> {
+ if (synchronous) {
+ throw failure;
+ }
+ return Mono.error(failure);
+ }, selector(PLUGIN_ID), rule(PLUGIN_ID)).block()));
+ return collector.log.get();
+ }
+
+ private ShenyuRequestLog execute(final String pluginId, final String
requestBody, final String responseBody) {
+ return execute(pluginId, createExchange(requestBody), requestBody,
responseBody);
+ }
+
+ private ShenyuRequestLog execute(final String pluginId, final
MockServerWebExchange exchange,
+ final String requestBody, final String...
responseChunks) {
+ AtomicReference<String> forwardedRequest = new AtomicReference<>();
+ plugin.doExecute(exchange, decorated -> forward(decorated,
forwardedRequest, responseChunks), selector(pluginId), rule(pluginId)).block();
+ String responseBody = String.join("", responseChunks);
+ assertEquals(requestBody, forwardedRequest.get());
+ assertEquals(responseBody,
exchange.getResponse().getBodyAsString().block());
+ ShenyuRequestLog log = collector.log.get();
+ assertNotNull(log);
+ assertEquals(HttpStatus.CREATED.value(), log.getStatus());
+ assertEquals(responseBody.getBytes(StandardCharsets.UTF_8).length,
log.getResponseContentLength());
+ return log;
+ }
+
+ private Mono<Void> forward(final ServerWebExchange exchange, final
AtomicReference<String> forwardedRequest,
+ final String... responseChunks) {
+ return
DataBufferUtils.join(exchange.getRequest().getBody()).doOnNext(buffer -> {
+ forwardedRequest.set(buffer.toString(StandardCharsets.UTF_8));
+ DataBufferUtils.release(buffer);
+ }).then(Mono.defer(() -> {
+ exchange.getResponse().setStatusCode(HttpStatus.CREATED);
+
exchange.getResponse().getHeaders().setContentType(MediaType.TEXT_PLAIN);
+ return exchange.getResponse().writeWith(buffers(responseChunks));
+ }));
+ }
+
+ private MockServerWebExchange createExchange(final String... chunks) {
+ MockServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.post("http://localhost/body-limit")
+ .remoteAddress(new InetSocketAddress("127.0.0.1",
8000)).contentType(MediaType.TEXT_PLAIN).body(buffers(chunks)));
+ ShenyuContext context = new ShenyuContext();
+ context.setStartDateTime(LocalDateTime.now());
+ exchange.getAttributes().put(Constants.CONTEXT, context);
+ return exchange;
+ }
+
+ private Flux<DataBuffer> buffers(final String... chunks) {
+ return Flux.fromArray(chunks).map(chunk ->
DefaultDataBufferFactory.sharedInstance.wrap(chunk.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ private void updatePluginConfig(final String pluginId, final int
requestLimit, final int responseLimit) {
+ updatePluginConfig(pluginId, "{\"maxRequestBody\":" + requestLimit +
",\"maxResponseBody\":" + responseLimit + "}");
+ }
+
+ private void updatePluginConfig(final String pluginId, final String
configJson) {
+ PluginData pluginData = new PluginData();
+ pluginData.setId(pluginId);
+ pluginData.setName(PluginEnum.LOGGING_KAFKA.getName());
+ pluginData.setEnabled(true);
+ pluginData.setConfig(configJson);
+ dataHandler.handlerPlugin(pluginData);
+ }
+
+ private SelectorData selector(final String pluginId) {
+ SelectorData selector = new SelectorData();
+ selector.setId(pluginId + "-selector");
+ selector.setPluginId(pluginId);
+ return selector;
+ }
+
+ private RuleData rule(final String pluginId) {
+ RuleData rule = new RuleData();
+ rule.setId(pluginId + "-rule");
+ rule.setSelectorId(pluginId + "-selector");
+ return rule;
+ }
+
+ // Keep the handler's Singleton entry separate from production
configuration classes.
+ private static final class TestGlobalConfig extends GenericGlobalConfig {
+ }
+
+ private static final class TestLogPluginDataHandler extends
AbstractLogPluginDataHandler<TestGlobalConfig, GenericApiConfig> {
+
+ private final LogCollector<ShenyuRequestLog> collector;
+
+ private TestLogPluginDataHandler(final LogCollector<ShenyuRequestLog>
collector) {
+ this.collector = collector;
+ }
+
+ @Override
+ protected LogCollector<ShenyuRequestLog> logCollector() {
+ return collector;
+ }
+
+ @Override
+ protected void doRefreshConfig(final TestGlobalConfig globalLogConfig)
{
+ }
+
+ @Override
+ public String pluginNamed() {
+ return PluginEnum.LOGGING_KAFKA.getName();
+ }
+ }
+
+ private static final class TestLoggingPlugin extends
AbstractLoggingPlugin<ShenyuRequestLog> {
+
+ private final LogCollector<ShenyuRequestLog> collector;
+
+ private TestLoggingPlugin(final LogCollector<ShenyuRequestLog>
collector) {
+ this.collector = collector;
+ }
+
+ @Override
+ protected LogCollector<ShenyuRequestLog> logCollector() {
+ return collector;
+ }
+
+ @Override
+ protected PluginEnum pluginEnum() {
+ return PluginEnum.LOGGING_KAFKA;
+ }
+
+ @Override
+ protected ShenyuRequestLog doLogExecute(final ServerWebExchange
exchange, final SelectorData selector, final RuleData rule) {
+ return new ShenyuRequestLog();
+ }
+ }
+
+ private static final class RecordingLogCollector implements
LogCollector<ShenyuRequestLog> {
+
+ private final AtomicReference<ShenyuRequestLog> log = new
AtomicReference<>();
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public void desensitize(final ShenyuRequestLog requestLog, final
KeyWordMatch keyWordMatch, final String desensitizeAlg) {
+ }
+
+ @Override
+ public void collect(final ShenyuRequestLog requestLog) {
+ log.set(requestLog);
+ }
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/BodyWriterTest.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/BodyWriterTest.java
index e2f1824f97..927d74a2d4 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/BodyWriterTest.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/BodyWriterTest.java
@@ -20,10 +20,19 @@ package org.apache.shenyu.plugin.logging.common.body;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.test.util.ReflectionTestUtils;
+import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
/**
* The Test Case For BodyWriter.
@@ -69,4 +78,167 @@ public class BodyWriterTest {
String res = writer.output();
Assertions.assertEquals(res, "hello, shenyu");
}
+
+ @Test
+ public void testSplitUtf8BodyAtLimit() {
+ BodyWriter limitedWriter = new BodyWriter(6);
+ // two 3-byte UTF-8 characters, split inside the first character
+ final String body = Character.toString(0x20AC) +
Character.toString(0x2122);
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ limitedWriter.write(ByteBuffer.wrap(bytes, 0, 1).asReadOnlyBuffer());
+ limitedWriter.write(ByteBuffer.wrap(bytes, 1, bytes.length -
1).asReadOnlyBuffer());
+ Assertions.assertFalse(limitedWriter.isLimitExceeded());
+ Assertions.assertEquals(6, limitedWriter.size());
+ Assertions.assertEquals(body, limitedWriter.output());
+ }
+
+ @Test
+ public void testOverflowDropsBufferAndKeepsCounting() {
+ BodyWriter limitedWriter = new BodyWriter(4);
+
limitedWriter.write(ByteBuffer.wrap("test".getBytes(StandardCharsets.UTF_8)));
+ Assertions.assertFalse(limitedWriter.isLimitExceeded());
+ Assertions.assertEquals(4, bufferedSize(limitedWriter));
+ limitedWriter.write(ByteBuffer.wrap(new byte[1]));
+ Assertions.assertTrue(limitedWriter.isLimitExceeded());
+ assertBufferReleased(limitedWriter);
+ for (int i = 0; i < 64; i++) {
+ limitedWriter.write(ByteBuffer.wrap(new byte[1024]));
+ }
+ Assertions.assertEquals(5 + 64 * 1024, limitedWriter.size());
+ assertBufferReleased(limitedWriter);
+ Assertions.assertFalse(limitedWriter.isEmpty());
+ Assertions.assertEquals("", limitedWriter.output());
+ }
+
+ @Test
+ public void testOversizedChunkIsNotBufferedAndSourceUntouched() {
+ BodyWriter limitedWriter = new BodyWriter(4);
+ ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
+ limitedWriter.write(buffer.asReadOnlyBuffer());
+ Assertions.assertEquals(0, buffer.position());
+ Assertions.assertEquals(1024 * 1024, limitedWriter.size());
+ Assertions.assertTrue(limitedWriter.isLimitExceeded());
+ assertBufferReleased(limitedWriter);
+ Assertions.assertEquals("", limitedWriter.output());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, -1, Integer.MIN_VALUE})
+ public void testNonPositiveLimitBuffersNothing(final int limit) {
+ BodyWriter limitedWriter = new BodyWriter(limit);
+ limitedWriter.write(ByteBuffer.allocate(0));
+ Assertions.assertTrue(limitedWriter.isEmpty());
+ Assertions.assertFalse(limitedWriter.isLimitExceeded());
+
limitedWriter.write(ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+ Assertions.assertEquals(5, limitedWriter.size());
+ Assertions.assertTrue(limitedWriter.isLimitExceeded());
+ assertBufferReleased(limitedWriter);
+ Assertions.assertEquals("", limitedWriter.output());
+ }
+
+ @Test
+ public void testObservedSizeIsCapped() {
+ BodyWriter limitedWriter = new BodyWriter(4);
+ ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
+ for (int i = 0; i < 2049; i++) {
+ limitedWriter.write(buffer.asReadOnlyBuffer());
+ }
+ Assertions.assertEquals(Integer.MAX_VALUE, limitedWriter.size());
+ Assertions.assertTrue(limitedWriter.isLimitExceeded());
+ Assertions.assertEquals("", limitedWriter.output());
+ }
+
+ @Test
+ public void testOverflowDetectedWhenSizeSaturatesAtMaxLimit() {
+ BodyWriter maxLimitWriter = new BodyWriter(Integer.MAX_VALUE);
+ // simulate a body that has already reached the int range without
allocating 2 GiB
+ ReflectionTestUtils.setField(maxLimitWriter, "size", (long)
Integer.MAX_VALUE - 1);
+ maxLimitWriter.write(ByteBuffer.wrap(new byte[2]));
+ Assertions.assertEquals(Integer.MAX_VALUE, maxLimitWriter.size());
+ Assertions.assertTrue(maxLimitWriter.isLimitExceeded());
+ assertBufferReleased(maxLimitWriter);
+ Assertions.assertEquals("", maxLimitWriter.output());
+ }
+
+ @Test
+ public void testOutputStopsFurtherWrites() {
+ BodyWriter limitedWriter = new BodyWriter(4);
+
limitedWriter.write(ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8)));
+ Assertions.assertEquals("abc", limitedWriter.output());
+
limitedWriter.write(ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+ Assertions.assertEquals(3, limitedWriter.size());
+ Assertions.assertFalse(limitedWriter.isLimitExceeded());
+ Assertions.assertEquals("abc", limitedWriter.output());
+ }
+
+ @Test
+ public void testConcurrentWriteAndOutputDoNotFail() throws Exception {
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try {
+ for (int round = 0; round < 200; round++) {
+ BodyWriter concurrentWriter = new BodyWriter(64);
+ CountDownLatch start = new CountDownLatch(1);
+ Future<?> writes = executor.submit(() -> {
+ start.await();
+ for (int i = 0; i < 100; i++) {
+
concurrentWriter.write(ByteBuffer.wrap("abcdefgh".getBytes(StandardCharsets.UTF_8)));
+ }
+ return null;
+ });
+ Future<String> output = executor.submit(() -> {
+ start.await();
+ return concurrentWriter.output();
+ });
+ start.countDown();
+ writes.get(5, TimeUnit.SECONDS);
+ String body = output.get(5, TimeUnit.SECONDS);
+ // output sees either a consistent prefix within the limit, or
nothing after overflow
+ Assertions.assertTrue(body.length() <= 64 && body.length() % 8
== 0, body);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Test
+ public void testOverflowReleasesBufferReferences() {
+ BodyWriter limitedWriter = new BodyWriter(512 * 1024);
+ ByteBuffer chunk = ByteBuffer.allocate(8192);
+ for (int i = 0; i < 64; i++) {
+ limitedWriter.write(chunk.asReadOnlyBuffer());
+ }
+ Assertions.assertFalse(limitedWriter.isLimitExceeded());
+ Assertions.assertEquals(512 * 1024, bufferedSize(limitedWriter));
+ limitedWriter.write(ByteBuffer.wrap(new byte[1]));
+ Assertions.assertTrue(limitedWriter.isLimitExceeded());
+ // the writer no longer references the buffered array, so it is
collectable before the request ends
+ assertBufferReleased(limitedWriter);
+ // later writes only count bytes and must not allocate a new buffer
+ for (int i = 0; i < 8; i++) {
+ limitedWriter.write(chunk.asReadOnlyBuffer());
+ }
+ assertBufferReleased(limitedWriter);
+ Assertions.assertEquals(512 * 1024 + 1 + 8 * 8192,
limitedWriter.size());
+ Assertions.assertEquals("", limitedWriter.output());
+ assertBufferReleased(limitedWriter);
+ }
+
+ @Test
+ public void testEmptyBodyDoesNotAllocateBuffer() {
+ BodyWriter limitedWriter = new BodyWriter(4);
+ limitedWriter.write(ByteBuffer.allocate(0));
+ Assertions.assertNull(ReflectionTestUtils.getField(limitedWriter,
"stream"));
+ Assertions.assertEquals("", limitedWriter.output());
+ }
+
+ private static void assertBufferReleased(final BodyWriter bodyWriter) {
+ Assertions.assertNull(ReflectionTestUtils.getField(bodyWriter,
"stream"));
+ Assertions.assertNull(ReflectionTestUtils.getField(bodyWriter,
"channel"));
+ }
+
+ private static int bufferedSize(final BodyWriter bodyWriter) {
+ ByteArrayOutputStream stream = (ByteArrayOutputStream)
ReflectionTestUtils.getField(bodyWriter, "stream");
+ Assertions.assertNotNull(stream);
+ return stream.size();
+ }
}
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/LoggingBodySaturatedSizeTest.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/LoggingBodySaturatedSizeTest.java
new file mode 100644
index 0000000000..0569125f1b
--- /dev/null
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/body/LoggingBodySaturatedSizeTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.plugin.logging.common.body;
+
+import org.apache.shenyu.common.constant.Constants;
+import org.apache.shenyu.plugin.api.context.ShenyuContext;
+import org.apache.shenyu.plugin.logging.common.collector.LogCollector;
+import org.apache.shenyu.plugin.logging.common.entity.ShenyuRequestLog;
+import org.apache.shenyu.plugin.logging.desensitize.api.matcher.KeyWordMatch;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.MediaType;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import reactor.core.publisher.Flux;
+
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Collections;
+
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.when;
+
+/**
+ * Callers must rely on the writer's overflow state, because {@link
BodyWriter#size()} saturates at
+ * {@link Integer#MAX_VALUE} and cannot be compared against a limit of {@link
Integer#MAX_VALUE}.
+ */
+public final class LoggingBodySaturatedSizeTest {
+
+ @Test
+ public void testRequestBodyStaysNullWhenSaturatedWriterOverflows() {
+ ShenyuRequestLog log = new ShenyuRequestLog();
+ MockServerWebExchange exchange = createExchange();
+ try (MockedConstruction<BodyWriter> ignored =
mockConstruction(BodyWriter.class, (writer, context) ->
stubSaturatedOverflow(writer))) {
+ LoggingServerHttpRequest<ShenyuRequestLog> request = new
LoggingServerHttpRequest<>(exchange.getRequest(), log, Integer.MAX_VALUE);
+ request.getBody().blockLast();
+ }
+ Assertions.assertNull(log.getRequestBody());
+ }
+
+ @Test
+ public void testResponseBodyStaysNullWhenSaturatedWriterOverflows() {
+ ShenyuRequestLog log = new ShenyuRequestLog();
+ MockServerWebExchange exchange = createExchange();
+
exchange.getResponse().getHeaders().setContentType(MediaType.TEXT_PLAIN);
+ try (MockedConstruction<BodyWriter> ignored =
mockConstruction(BodyWriter.class, (writer, context) ->
stubSaturatedOverflow(writer))) {
+ final LogCollector<ShenyuRequestLog> noCollector = null;
+ LoggingServerHttpResponse<ShenyuRequestLog> response = new
LoggingServerHttpResponse<>(exchange.getResponse(), log,
+ noCollector, false, new
KeyWordMatch(Collections.emptySet()), "md5", Integer.MAX_VALUE);
+ response.setExchange(exchange);
+ response.writeWith(buffers()).block();
+ }
+ Assertions.assertNull(log.getResponseBody());
+ Assertions.assertEquals(Integer.MAX_VALUE,
log.getResponseContentLength());
+ }
+
+ private static void stubSaturatedOverflow(final BodyWriter writer) {
+ when(writer.size()).thenReturn(Integer.MAX_VALUE);
+ when(writer.isEmpty()).thenReturn(false);
+ when(writer.isLimitExceeded()).thenReturn(true);
+ when(writer.output()).thenReturn("");
+ }
+
+ private static MockServerWebExchange createExchange() {
+ MockServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.post("http://localhost/saturated")
+ .contentType(MediaType.TEXT_PLAIN).body(buffers()));
+ ShenyuContext context = new ShenyuContext();
+ context.setStartDateTime(LocalDateTime.now());
+ exchange.getAttributes().put(Constants.CONTEXT, context);
+ return exchange;
+ }
+
+ private static Flux<DataBuffer> buffers() {
+ return
Flux.just(DefaultDataBufferFactory.sharedInstance.wrap("hello".getBytes(StandardCharsets.UTF_8)));
+ }
+}
diff --git
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtilsTest.java
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtilsTest.java
index a6ab7e99a5..4fa3e728e7 100644
---
a/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtilsTest.java
+++
b/shenyu-plugin/shenyu-plugin-logging/shenyu-plugin-logging-common/src/test/java/org/apache/shenyu/plugin/logging/common/utils/LogCollectConfigUtilsTest.java
@@ -20,6 +20,7 @@ package org.apache.shenyu.plugin.logging.common.utils;
import org.apache.shenyu.common.constant.Constants;
import org.apache.shenyu.plugin.api.context.ShenyuContext;
import org.apache.shenyu.plugin.logging.common.config.GenericGlobalConfig;
+import
org.apache.shenyu.plugin.logging.common.handler.AbstractLogPluginDataHandler;
import org.apache.shenyu.plugin.logging.common.sampler.Sampler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -33,6 +34,9 @@ import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* The Test Case For LogCollectConfigUtils.
@@ -65,4 +69,46 @@ public class LogCollectConfigUtilsTest {
field.setAccessible(true);
assertEquals(field.get("const"), Sampler.ALWAYS_SAMPLE);
}
+
+ @Test
+ public void testGetGlobalConfigUsesPluginId() {
+ String pluginId = "config-utils-body-limit-test";
+ GenericGlobalConfig pluginConfig = new GenericGlobalConfig();
+ pluginConfig.setMaxRequestBody(8);
+ pluginConfig.setMaxResponseBody(16);
+ AbstractLogPluginDataHandler.getPluginGlobalConfigMap().put(pluginId,
pluginConfig);
+ try {
+ assertSame(pluginConfig,
LogCollectConfigUtils.getGlobalConfig(pluginId));
+ } finally {
+
AbstractLogPluginDataHandler.getPluginGlobalConfigMap().remove(pluginId);
+ }
+ }
+
+ @Test
+ public void testMissingConfigDoesNotShareMutableDefaults() {
+ String pluginId = "missing-body-limit-test";
+ GenericGlobalConfig missing =
LogCollectConfigUtils.getGlobalConfig(pluginId);
+ assertEquals(524288, missing.getMaxRequestBody());
+ assertEquals(524288, missing.getMaxResponseBody());
+ missing.setMaxRequestBody(1);
+ missing.setMaxResponseBody(1);
+ GenericGlobalConfig next =
LogCollectConfigUtils.getGlobalConfig(pluginId);
+ assertEquals(524288, next.getMaxRequestBody());
+ assertEquals(524288, next.getMaxResponseBody());
+ }
+
+ @Test
+ public void testNullPluginIdUsesDefaults() {
+ GenericGlobalConfig missing =
LogCollectConfigUtils.getGlobalConfig(null);
+ assertEquals(524288, missing.getMaxRequestBody());
+ assertEquals(524288, missing.getMaxResponseBody());
+ }
+
+ @Test
+ public void testLegacySizeChecksUseDefaultLimits() {
+ assertFalse(LogCollectConfigUtils.isRequestBodyTooLarge(524288));
+ assertTrue(LogCollectConfigUtils.isRequestBodyTooLarge(524289));
+ assertFalse(LogCollectConfigUtils.isResponseBodyTooLarge(524288));
+ assertTrue(LogCollectConfigUtils.isResponseBodyTooLarge(524289));
+ }
}