Ensure HTTP classes don't close streams

Prior to this commit several HTTP classes made use of FileCopyUtils
when reading from or writing to streams. This has the unfortunate
side effect of closing streams that should really be left open.

The problem is particularly noticeable when dealing with a
FormHttpMessageConverter that is writing a multi-part response.

Relevant HTTP classes have now been refactored to make use of a new
StreamUtils class that works in a similar way FileCopyUtils but does
not close streams.

The NonClosingOutputStream class from SimpleStreamingClientHttpRequest
has also been refactored to a StreamUtils method.

Issue: SPR-10095
This commit is contained in:
Phillip Webb
2013-02-11 18:42:21 -08:00
parent 08e1cbc02b
commit 6661788748
13 changed files with 360 additions and 80 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,7 @@ import java.net.URI;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* Simple implementation of {@link ClientHttpRequest} that wraps another request.
@@ -53,8 +53,7 @@ final class BufferingClientHttpRequestWrapper extends AbstractBufferingClientHtt
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
this.request.getHeaders().putAll(headers);
OutputStream body = this.request.getBody();
FileCopyUtils.copy(bufferedOutput, body);
StreamUtils.copy(bufferedOutput, this.request.getBody());
ClientHttpResponse response = this.request.execute();
return new BufferingClientHttpResponseWrapper(response);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.io.InputStream;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* Simple implementation of {@link ClientHttpResponse} that reads the request's body into memory,
@@ -61,7 +62,7 @@ final class BufferingClientHttpResponseWrapper implements ClientHttpResponse {
public InputStream getBody() throws IOException {
if (this.body == null) {
this.body = FileCopyUtils.copyToByteArray(this.response.getBody());
this.body = StreamUtils.copyToByteArray(this.response.getBody());
}
return new ByteArrayInputStream(this.body);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,7 +24,7 @@ import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* Wrapper for a {@link ClientHttpRequest} that has support for {@link ClientHttpRequestInterceptor}s.
@@ -86,7 +86,7 @@ class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
delegate.getHeaders().putAll(request.getHeaders());
if (body.length > 0) {
FileCopyUtils.copy(body, delegate.getBody());
StreamUtils.copy(body, delegate.getBody());
}
return delegate.execute();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.http.client;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
@@ -27,6 +26,7 @@ import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.StreamUtils;
/**
* {@link ClientHttpRequest} implementation that uses standard J2SE facilities to execute streaming requests.
@@ -77,7 +77,7 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest {
this.connection.connect();
this.body = this.connection.getOutputStream();
}
return new NonClosingOutputStream(this.body);
return StreamUtils.nonClosing(this.body);
}
private void writeHeaders(HttpHeaders headers) {
@@ -106,26 +106,4 @@ final class SimpleStreamingClientHttpRequest extends AbstractClientHttpRequest {
return new SimpleClientHttpResponse(this.connection);
}
private static class NonClosingOutputStream extends FilterOutputStream {
private NonClosingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(byte[] b) throws IOException {
super.write(b);
}
@Override
public void write(byte[] b, int off, int let) throws IOException {
out.write(b, off, let);
}
@Override
public void close() throws IOException {
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ import java.io.IOException;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* Implementation of {@link HttpMessageConverter} that can read and write byte arrays.
@@ -49,14 +49,9 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
@Override
public byte[] readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException {
long contentLength = inputMessage.getHeaders().getContentLength();
if (contentLength >= 0) {
ByteArrayOutputStream bos = new ByteArrayOutputStream((int) contentLength);
FileCopyUtils.copy(inputMessage.getBody(), bos);
return bos.toByteArray();
}
else {
return FileCopyUtils.copyToByteArray(inputMessage.getBody());
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(contentLength >= 0 ? (int) contentLength : StreamUtils.BUFFER_SIZE);
StreamUtils.copy(inputMessage.getBody(), bos);
return bos.toByteArray();
}
@Override
@@ -66,7 +61,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
@Override
protected void writeInternal(byte[] bytes, HttpOutputMessage outputMessage) throws IOException {
FileCopyUtils.copy(bytes, outputMessage.getBody());
StreamUtils.copy(bytes, outputMessage.getBody());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
package org.springframework.http.converter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@@ -37,9 +36,9 @@ import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
@@ -170,7 +169,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
String body = FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
@@ -246,7 +245,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
}
byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
FileCopyUtils.copy(bytes, outputMessage.getBody());
StreamUtils.copy(bytes, outputMessage.getBody());
}
private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.http.converter;
import java.io.IOException;
import java.io.InputStream;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
@@ -28,7 +29,7 @@ import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
@@ -61,7 +62,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
byte[] body = FileCopyUtils.copyToByteArray(inputMessage.getBody());
byte[] body = StreamUtils.copyToByteArray(inputMessage.getBody());
return new ByteArrayResource(body);
}
@@ -84,7 +85,7 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
FileCopyUtils.copy(resource.getInputStream(), outputMessage.getBody());
StreamUtils.copy(resource.getInputStream(), outputMessage.getBody());
outputMessage.getBody().flush();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,8 +17,6 @@
package org.springframework.http.converter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
@@ -27,7 +25,7 @@ import java.util.List;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
/**
* Implementation of {@link HttpMessageConverter} that can read and write strings.
@@ -84,7 +82,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
return FileCopyUtils.copyToString(new InputStreamReader(inputMessage.getBody(), charset));
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
@Override
@@ -105,7 +103,7 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset));
StreamUtils.copy(s, charset, outputMessage.getBody());
}
/**