It make sense that converters skip closing InputStream on client side to fix https://github.com/spring-projects/spring-framework/issues/27969, but on server side I suggest that converters shouldn't wrap InputStream StreamUtils::nonClosing, the change breaks existing behavior and maybe result in resource leak.
I've encountered a regression since v5.3.16 which introduced by https://github.com/spring-projects/spring-framework/commit/8d5a6520ce033c38c57a9ec026102e17bd598acc .
I'm wrapping HttpServletRequest overriding getInputStream() to return customized ContentCachingInputStream, which will caching request body and log it when close() is called. before that commit, jackson will close the underlying stream since AUTO_CLOSE_SOURCE default true.
class ContentCachingInputStream extends ServletInputStream {
private final Logger logger;
private final ServletInputStream is;
private final String characterEncoding;
private FastByteArrayOutputStream cachedContent;
public ContentCachingInputStream(ServletRequest request, Logger logger) throws IOException {
this.logger = logger;
this.is = request.getInputStream();
this.characterEncoding = request.getCharacterEncoding();
int contentLength = request.getContentLength();
this.cachedContent = new FastByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
}
@Override
public int readLine(byte[] b, final int off, final int len) throws IOException {
int count = is.readLine(b, off, len);
cache(b, off, count);
return count;
}
@Override
public int read(byte[] b, final int off, final int len) throws IOException {
int count = is.read(b, off, len);
cache(b, off, count);
return count;
}
private void cache(byte[] b, final int off, final int count) throws IOException {
if (count != -1)
cachedContent.write(b, off, count);
}
@Override
public int read() throws IOException {
int ch = this.is.read();
if (ch != -1)
cachedContent.write(ch);
return ch;
}
@Override
public boolean isFinished() {
return this.is.isFinished();
}
@Override
public boolean isReady() {
return this.is.isReady();
}
@Override
public void setReadListener(ReadListener readListener) {
this.is.setReadListener(readListener);
}
@Override
public void close() throws IOException {
try {
this.is.close();
} finally {
if (cachedContent != null) {
byte[] bytes = cachedContent.toByteArray();
cachedContent = null;
String str = new String(bytes, 0, bytes.length, characterEncoding);
logger.info("\n{}", str);
}
}
}
}
Workaround: overrding MappingJackson2HttpMessageConverter to force close after read.
MappingJackson2HttpMessageConverter jackson2 = new MappingJackson2HttpMessageConverter() {
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException {
try {
return super.read(type, contextClass, inputMessage);
} finally {
inputMessage.getBody().close();
}
}
@Override
public Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException {
try {
return super.readInternal(clazz, inputMessage);
} finally {
inputMessage.getBody().close();
}
}
};
Comment From: rstoyanchev
As you can see from https://github.com/spring-projects/spring-framework/issues/27969#issuecomment-1026711963, this is by design, and while it wasn't consistently enforced, it was the intent.