getReader() has already been called for this request 해결 방법
java.lang.IllegalStateException: getReader() has already been called for this request 의 원인과
해결방법을 알아보겠습니다.
원인
위의 Exception은 request.getReader()를 한번 이상 사용할 때 발생합니다.
request.getReader() 를 사용하게 되면 request body 를 읽기위한 스트림을 반환하고, 읽는동안 내부적으로 포인트를 사용하여 읽은 위치를 기억하게 됩니다.
처음 다 읽은 후 두번째 읽을 때는 이미 포인터가 body의 마지막부분을 기억하고 있기 때문에 읽을 데이터가 없다고 판단하게 되는 것이죠.
예를들어 Interceptor에서 아래와 같은 코드로 body의 데이터를 조회한다고 할 때
이미 인터셉터에서 request.getReader()로 body 데이터에 접근하기 때문에
Controller에서 @RequestBody로 접근을 하게되면 body에 데이터가 없어 body가 누락되었다고 판단하게 됩니다.
[Interceptor]
BufferedReader reader = request.getReader();
StringBuilder requestBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(requestBody.toString());
String col = jsonNode.get("col").asText();
[Controller]
@PostMapping(value = "/test", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "getReader() test", notes = "getReader() test")
@CustomApiResponse
public ResponseEntity<?> loginCheckByApple(@Valid @RequestBody TestDTO param) {
//...
}
Interceptor와 아래서 설명할 Filter에 대해서는 아래의 Link를 참고하세요!
Link : https://aljjabaegi.tistory.com/632
해결
HttpServletRequestWrapper를 사용하여 HttpRequest에서 inputStream을 읽어 저장 한 뒤 다음 요청부터는 저장된 inputStream을 복사하여 전달함으로써 문제를 해결할 수 있습니다.
1. HttpServletRequestWrapper 구현
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.commons.lang3.StringUtils;
import org.zeroturnaround.zip.commons.IOUtils;
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private final Charset encoding;
private byte[] rawData;
public CustomRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String characterEncoding = request.getCharacterEncoding();
if (StringUtils.isBlank(characterEncoding)) {
characterEncoding = StandardCharsets.UTF_8.name();
}
this.encoding = Charset.forName(characterEncoding);
try {
InputStream inputStream = request.getInputStream();
this.rawData = IOUtils.toByteArray(inputStream);
} catch (IOException e) {
throw e;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
}
@Override
public ServletRequest getRequest() {
return super.getRequest();
}
}
2. Filter 등록
특정 URI에서만 동작, 혹은 모든 URI에서 동작하도록 Filter를 등록하여 filter에서 CustomRequestWrapper를 생성해 전달합니다. (/test/...로 시작하는 모든 URI에서 동작)
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
@WebFilter(urlPatterns="/test/*")
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
CustomRequestWrapper rereadableRequestWrapper = new CustomRequestWrapper((HttpServletRequest) request);
chain.doFilter(rereadableRequestWrapper, response);
}
}
Filter 동작을 위해서 Springboot application main class에서 @ServletComponentScan을 추가합니다.
@SpringBootApplication
@ServletComponentScan
public class TestApplication extends SpringBootServletInitializer {
//...
}
참고 : https://meetup.nhncloud.com/posts/44