fix(idempotency): replace ContentCachingRequestWrapper with re-readable CachedBodyHttpServletRequest
ContentCachingRequestWrapper только cache'ит body ПОСЛЕ того как handler прочитает getInputStream(). Если filter читает body первым (для hash), handler потом получает 'Required request body is missing' — буфер пустой. CachedBodyHttpServletRequest при construction читает body в byte[] и getInputStream() возвращает свежий ByteArrayInputStream при каждом вызове. Filter и handler читают одни и те же байты независимо.
This commit is contained in:
+59
@@ -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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-6
@@ -12,7 +12,6 @@ import org.springframework.http.HttpHeaders;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
import org.springframework.web.util.ContentCachingRequestWrapper;
|
|
||||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -71,11 +70,8 @@ public class IdempotencyFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentCachingRequestWrapper wrappedReq = new ContentCachingRequestWrapper(request);
|
CachedBodyHttpServletRequest wrappedReq = new CachedBodyHttpServletRequest(request);
|
||||||
// Need to read body to compute hash; do it after first content read. Spring caches via
|
byte[] body = wrappedReq.getCachedBody();
|
||||||
// ContentCachingRequestWrapper after handler reads, so we trigger the read manually.
|
|
||||||
wrappedReq.getInputStream().readAllBytes(); // populate cache
|
|
||||||
byte[] body = wrappedReq.getContentAsByteArray();
|
|
||||||
String hash = service.hashRequest(body);
|
String hash = service.hashRequest(body);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user