From dfb694a42fe3f8e7d09e4f2b4b5a9a297b13d995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Mon, 4 May 2026 00:15:11 +0300 Subject: [PATCH] fix(idempotency): replace ContentCachingRequestWrapper with re-readable CachedBodyHttpServletRequest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ContentCachingRequestWrapper только cache'ит body ПОСЛЕ того как handler прочитает getInputStream(). Если filter читает body первым (для hash), handler потом получает 'Required request body is missing' — буфер пустой. CachedBodyHttpServletRequest при construction читает body в byte[] и getInputStream() возвращает свежий ByteArrayInputStream при каждом вызове. Filter и handler читают одни и те же байты независимо. --- .../CachedBodyHttpServletRequest.java | 59 +++++++++++++++++++ .../idempotency/IdempotencyFilter.java | 8 +-- 2 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/CachedBodyHttpServletRequest.java diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/CachedBodyHttpServletRequest.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/CachedBodyHttpServletRequest.java new file mode 100644 index 0000000..42bcbe5 --- /dev/null +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/CachedBodyHttpServletRequest.java @@ -0,0 +1,59 @@ +package cloud.nstart.terravault.ordinis.restapi.idempotency; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.springframework.util.StreamUtils; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * Кэширует тело запроса в памяти, чтобы его можно было прочитать несколько раз + * (для idempotency hash + handler-аргумента). В отличие от + * {@link org.springframework.web.util.ContentCachingRequestWrapper}, отдает + * свежий InputStream при каждом вызове {@link #getInputStream()}. + */ +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private final byte[] cachedBody; + + public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException { + super(request); + InputStream is = request.getInputStream(); + this.cachedBody = StreamUtils.copyToByteArray(is); + } + + public byte[] getCachedBody() { + return cachedBody; + } + + @Override + public ServletInputStream getInputStream() { + return new CachedServletInputStream(cachedBody); + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); + } + + private static class CachedServletInputStream extends ServletInputStream { + private final ByteArrayInputStream buf; + + CachedServletInputStream(byte[] data) { + this.buf = new ByteArrayInputStream(data); + } + + @Override public boolean isFinished() { return buf.available() == 0; } + @Override public boolean isReady() { return true; } + @Override public void setReadListener(ReadListener listener) { /* no-op */ } + @Override public int read() { return buf.read(); } + @Override public int read(byte[] b, int off, int len) { return buf.read(b, off, len); } + } +} diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/IdempotencyFilter.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/IdempotencyFilter.java index 6d21939..f9cf24a 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/IdempotencyFilter.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/idempotency/IdempotencyFilter.java @@ -12,7 +12,6 @@ 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; import org.springframework.web.util.ContentCachingResponseWrapper; import java.io.IOException; @@ -71,11 +70,8 @@ public class IdempotencyFilter extends OncePerRequestFilter { return; } - ContentCachingRequestWrapper wrappedReq = new ContentCachingRequestWrapper(request); - // Need to read body to compute hash; do it after first content read. Spring caches via - // ContentCachingRequestWrapper after handler reads, so we trigger the read manually. - wrappedReq.getInputStream().readAllBytes(); // populate cache - byte[] body = wrappedReq.getContentAsByteArray(); + CachedBodyHttpServletRequest wrappedReq = new CachedBodyHttpServletRequest(request); + byte[] body = wrappedReq.getCachedBody(); String hash = service.hashRequest(body); try {