RestTemplate response.getBody throws exception on

2019-02-15 18:24发布

问题:

I am trying to intercept and log all the request-responses. To make requests i am using RestTemplate.exchange().

When i make a GET request and get an 4** error i can call the ClientHttpResponse.getBody() and can access the response body but for PUT and POST requests ClientHttpResponse.getBody() method throws an exception.

What might be causing this and how can i get the response body for POST and PUT requests as well?

This is where i make the request:

apiResponse = restTemplate.exchange(url, vCloudRequest.getHttpMethod(), entity, responseType);

This is the part of the interceptor that gets the exception:

@Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        ClientHttpResponse response = execution.execute(request, body);

        String requestString = new String(body);

        String responseString = new 
// Below line throws exception
String(ByteStreams.toByteArray(response.getBody()), Charset.forName("UTF-8"));

This is the stack.

Caused by: java.io.IOException: Server returned HTTP response code: 403 for URL: https://176.235.57.11/api/admin/org/bd154aaf-2e7c-446d-91be-f0a45138476b/users
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1876)
    at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
    at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
    at org.springframework.http.client.SimpleClientHttpResponse.getBody(SimpleClientHttpResponse.java:85)
    at org.springframework.http.client.BufferingClientHttpResponseWrapper.getBody(BufferingClientHttpResponseWrapper.java:69)
    at roma.api_utils.model.Interceptors.RequestLoggingInterceptor.intercept(RequestLoggingInterceptor.java:39)
    at org.springframework.http.client.InterceptingClientHttpRequest$InterceptingRequestExecution.execute(InterceptingClientHttpRequest.java:86)
    at org.springframework.http.client.InterceptingClientHttpRequest.executeInternal(InterceptingClientHttpRequest.java:70)
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:652)

Update :

When i call response.getStatusCode() before calling response.getBody() it doesn't throw IOException.

回答1:

Basic knowledge:

HttpURLConnection has two similar fields,errorStream and inputStream,When we invoke its getInputSteam method , it checks whether the response has an error code.if so,it throws an IOException and records it,that's why you got the exception;Further more,it also copy the contents in inputStream to errorStream,thus we can get its response body by invoking its getErrorStream method,which is exactly what SimpleClientHttpResponse does with its getBody method:

    @Override
    public InputStream getBody() throws IOException {
        InputStream errorStream = this.connection.getErrorStream();
        this.responseStream = 
(errorStream != null ? errorStream : this.connection.getInputStream());
        return this.responseStream;
    }

it first checks if errorStream is null,if false,return it ,if true,call connection.getInputStream() and return

Now here is the answer

  1. Why calling response.getBody() doesn't throw IOException after you called response.getStatusCode(),it is because getStatusCode calls getInputStream internally.Thus,errorStream will be not null when getBody is called
  2. Why it doesn't throw an exception when the http method is GET. check method org.springframework.http.client.SimpleBufferingClientHttpRequest#executeInternal.

.

@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) 
throws IOException {
    addHeaders(this.connection, headers);
    // JDK <1.8 doesn't support getOutputStream with HTTP DELETE
    if (HttpMethod.DELETE == getMethod() && bufferedOutput.length == 0) {
        this.connection.setDoOutput(false);
    }
    if (this.connection.getDoOutput() && this.outputStreaming) {
        this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
    }
    this.connection.connect();
    if (this.connection.getDoOutput()) {
        FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
    }
    else {
        // Immediately trigger the request in a no-output scenario as well
        this.connection.getResponseCode();
    }
    return new SimpleClientHttpResponse(this.connection);
}

it eagerly executes this.connection.getResponseCode(); when http method is GET



回答2:

I had a similar requirement of logging every request and response. I wrote a filter and hooked into the filter chain.

The code looks like something below:

public class CustomRequestFilter implements Filter {


  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    //No custom initialisation required
  }

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
      FilterChain filterChain) throws IOException, ServletException {
    try {

      HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
      HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

        Map<String, String> requestMap = this
            .getTypesafeRequestMap(httpServletRequest);
        BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
            httpServletRequest);
        BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
            httpServletResponse);

        final StringBuilder logMessage = new StringBuilder(
            "REST Request - ").append("[HTTP METHOD:")
            .append(httpServletRequest.getMethod())
            .append("] [PATH INFO:")
            .append(httpServletRequest.getServletPath())
            .append("] [REQUEST PARAMETERS:").append(requestMap)
            .append("] [REQUEST BODY:")
            .append(bufferedRequest.getRequestBody())
            .append("] [REMOTE ADDRESS:")
            .append(httpServletRequest.getRemoteAddr()).append("]");
        log.info("=======================REQUEST PAYLOAD=================================");
        log.info(bufferedRequest.getRequestBody());
        log.info("========================================================");
        filterChain.doFilter(bufferedRequest, bufferedResponse);
        logMessage.append(" [RESPONSE:")
            .append(bufferedResponse.getContent()).append("]");
        log.info("=======================REST RESPONSE=================================");
        log.info(bufferedResponse.getContent());
        log.info("========================================================");
             } catch (Exception a) {
      log.error("Error while filtering ", a);
    }
  }

  private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
    Map<String, String> typesafeRequestMap = new HashMap<>();
    Enumeration<?> requestParamNames = request.getParameterNames();
    while (requestParamNames.hasMoreElements()) {
      String requestParamName = (String) requestParamNames.nextElement();
      String requestParamValue;
      if ("password".equalsIgnoreCase(requestParamName)) {
        requestParamValue = "********";
      } else {
        requestParamValue = request.getParameter(requestParamName);
      }
      typesafeRequestMap.put(requestParamName, requestParamValue);
    }
    return typesafeRequestMap;
  }

  @Override
  public void destroy() {
    //not yet implemented
  }

  private static final class BufferedRequestWrapper extends
      HttpServletRequestWrapper {

    private ByteArrayInputStream bais = null;
    private ByteArrayOutputStream baos = null;
    private BufferedServletInputStream bsis = null;
    private byte[] buffer = null;

    public BufferedRequestWrapper(HttpServletRequest req)
        throws IOException {
      super(req);
      // Read InputStream and store its content in a buffer.
      InputStream is = req.getInputStream();
      this.baos = new ByteArrayOutputStream();
      byte[] buf = new byte[1024];
      int read;
      while ((read = is.read(buf)) > 0) {
        this.baos.write(buf, 0, read);
      }
      this.buffer = this.baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() {
      this.bais = new ByteArrayInputStream(this.buffer);
      this.bsis = new BufferedServletInputStream(this.bais);
      return this.bsis;
    }

    String getRequestBody() throws IOException {
      BufferedReader reader = new BufferedReader(new InputStreamReader(
          this.getInputStream()));
      String line;
      StringBuilder inputBuffer = new StringBuilder();
      do {
        line = reader.readLine();
        if (null != line) {
          inputBuffer.append(line.trim());
        }
      } while (line != null);
      reader.close();
      return inputBuffer.toString().trim();
    }

  }

  private static final class BufferedServletInputStream extends
      ServletInputStream {

    private ByteArrayInputStream bais;

    public BufferedServletInputStream(ByteArrayInputStream bais) {
      this.bais = bais;
    }

    @Override
    public int available() {
      return this.bais.available();
    }

    @Override
    public int read() {
      return this.bais.read();
    }

    @Override
    public int read(byte[] buf, int off, int len) {
      return this.bais.read(buf, off, len);
    }

    @Override
    public boolean isFinished() {
      return false;
    }

    @Override
    public boolean isReady() {
      return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {
        //No specific readListener changes required
    }
  }

  public class TeeServletOutputStream extends ServletOutputStream {

    private final TeeOutputStream targetStream;

    public TeeServletOutputStream(OutputStream one, OutputStream two) {
      targetStream = new TeeOutputStream(one, two);
    }

    @Override
    public void write(int arg0) throws IOException {
      this.targetStream.write(arg0);
    }

    @Override
    public void flush() throws IOException {
      super.flush();
      this.targetStream.flush();
    }

    @Override
    public void close() throws IOException {
      super.close();
      this.targetStream.close();
    }

    @Override
    public boolean isReady() {
      return false;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
      //not yet implemented
    }
  }

  public class BufferedResponseWrapper implements HttpServletResponse {

    HttpServletResponse original;
    TeeServletOutputStream tee;
    ByteArrayOutputStream bos;

    public BufferedResponseWrapper(HttpServletResponse response) {
      original = response;
    }

    public String getContent() {
      return bos.toString();
    }

    @Override
    public PrintWriter getWriter() throws IOException {
      return original.getWriter();
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
      if (tee == null) {
        bos = new ByteArrayOutputStream();
        tee = new TeeServletOutputStream(original.getOutputStream(),
            bos);
      }
      return tee;

    }

    @Override
    public String getCharacterEncoding() {
      return original.getCharacterEncoding();
    }

    @Override
    public String getContentType() {
      return original.getContentType();
    }

    @Override
    public void setCharacterEncoding(String charset) {
      original.setCharacterEncoding(charset);
    }

    @Override
    public void setContentLength(int len) {
      original.setContentLength(len);
    }

    @Override
    public void setContentLengthLong(long l) {
      original.setContentLengthLong(l);
    }

    @Override
    public void setContentType(String type) {
      original.setContentType(type);
    }

    @Override
    public void setBufferSize(int size) {
      original.setBufferSize(size);
    }

    @Override
    public int getBufferSize() {
      return original.getBufferSize();
    }

    @Override
    public void flushBuffer() throws IOException {
      tee.flush();
    }

    @Override
    public void resetBuffer() {
      original.resetBuffer();
    }

    @Override
    public boolean isCommitted() {
      return original.isCommitted();
    }

    @Override
    public void reset() {
      original.reset();
    }

    @Override
    public void setLocale(Locale loc) {
      original.setLocale(loc);
    }

    @Override
    public Locale getLocale() {
      return original.getLocale();
    }

    @Override
    public void addCookie(Cookie cookie) {
      original.addCookie(cookie);
    }

    @Override
    public boolean containsHeader(String name) {
      return original.containsHeader(name);
    }

    @Override
    public String encodeURL(String url) {
      return original.encodeURL(url);
    }

    @Override
    public String encodeRedirectURL(String url) {
      return original.encodeRedirectURL(url);
    }

    @SuppressWarnings("deprecation")
    @Override
    public String encodeUrl(String url) {
      return original.encodeUrl(url);
    }

    @SuppressWarnings("deprecation")
    @Override
    public String encodeRedirectUrl(String url) {
      return original.encodeRedirectUrl(url);
    }

    @Override
    public void sendError(int sc, String msg) throws IOException {
      original.sendError(sc, msg);
    }

    @Override
    public void sendError(int sc) throws IOException {
      original.sendError(sc);
    }

    @Override
    public void sendRedirect(String location) throws IOException {
      original.sendRedirect(location);
    }

    @Override
    public void setDateHeader(String name, long date) {
      original.setDateHeader(name, date);
    }

    @Override
    public void addDateHeader(String name, long date) {
      original.addDateHeader(name, date);
    }

    @Override
    public void setHeader(String name, String value) {
      original.setHeader(name, value);
    }

    @Override
    public void addHeader(String name, String value) {
      original.addHeader(name, value);
    }

    @Override
    public void setIntHeader(String name, int value) {
      original.setIntHeader(name, value);
    }

    @Override
    public void addIntHeader(String name, int value) {
      original.addIntHeader(name, value);
    }

    @Override
    public void setStatus(int sc) {
      original.setStatus(sc);
    }

    @SuppressWarnings("deprecation")
    @Override
    public void setStatus(int sc, String sm) {
      original.setStatus(sc, sm);
    }

    @Override
    public String getHeader(String arg0) {
      return original.getHeader(arg0);
    }

    @Override
    public Collection<String> getHeaderNames() {
      return original.getHeaderNames();
    }

    @Override
    public Collection<String> getHeaders(String arg0) {
      return original.getHeaders(arg0);
    }

    @Override
    public int getStatus() {
      return original.getStatus();
    }

  }
}


回答3:

For PUT and POST, it depends on your target resource. If your target resource do not add anything in the response's body after a PUT or POST request, it's normal to get an exception. In general, you know the resource that you send with PUT or POST so you can just check the response's status to know if your resource had been created or modified. You do not need to check the response body again.



回答4:

You can use the following to accesss the response body in the interceptor. I did a quick unit test to confirm it works even on a POST with a 403 response.

However be careful, getBody returns an InputStream. Which means you can only read it once. You won't be able to read the same stream again outside the interceptor unless you provide a new response with a new body.

...
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    final ClientHttpResponse response = execution.execute(request, body);
    final InputStream body = response.getBody();
    return response;
}
...