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.