Affects: Spring Framework : 5.2.1.RELEASE Spring Boot : 2.1.5.RELEASE

Added ExchangeFilterFunction to WebClient which logs request and response but while logging, there is no handle to get the request/response body as a string or JSON.

public static ExchangeFilterFunction logRequest() {
    return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
             //body
             logger.debug(clientRequest.body()); //-> This returns BodyInserter<?, ? super ClientHttpRequest>, need to fetch the body as a String/JSON.
             return Mono.just(clientRequest);
             }
}

Comment From: rstoyanchev

ClientRequest is simply a common representation of the application input for the request which could be one or more Objects for the body. Those are yet to be serialized through a HttpMessageWriter and written to an HTTP library specific implementation of ClientHttpRequest. That means ClientRequest does not have the low level content.

Logging would need to be performed at a lower level which could be done by wrapping the ClientHttpRequest:

class LoggingClientHttpRequestDecorator extends ClientHttpRequestDecorator {

    public LoggingClientHttpRequestWrapper(ClientHttpRequest delegate) {
        super(delegate);
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        // optionally aggregate for full body content
        body = DataBufferUtils.join(body)
                .doOnNext(content -> {
                    // log here...
                });
        return super.writeWith(body);
    }
}

and then applying that at the connector level:

class LoggingClientHttpConnectorDecorator implements ClientHttpConnector {

    private final ClientHttpConnector delegate;

    public LoggingClientHttpConnectorWrapper(ClientHttpConnector delegate) {
        this.delegate = delegate;
    }

    @Override
    public Mono<ClientHttpResponse> connect(HttpMethod method, URI uri,
            Function<? super ClientHttpRequest, Mono<Void>> callback) {

        return this.delegate.connect(method, uri,
                request -> callback.apply(new LoggingClientHttpRequestDecorator(request)));
    }
}

Comment From: ashoke-cube

Thank you for the response. I have a query on the writeWith() implementation. I have the following implementation as opposed to your suggestion.

Does the below implementation wait for the full body content before logging? Or it logs as and when the data is available? I would like the full body to be available before logging. If that is needed should I follow your way of joining the body?

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        return super.writeWith(Flux.from(body).map(writeToLog()));
    }

        private Function<DataBuffer, DataBuffer> writeToLog() {
        return dataBuffer -> {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                Channels.newChannel(baos).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
                log(baos.toString());
            } catch (IOException e) {
                LOGGER.error("Unable to log input response due to an error", e);
            } finally {
                try {
                    baos.close();
                } catch (IOException e) {
                    LOGGER.error("Unable to log input response due to an error", e);
                }
            }
            return dataBuffer;
        };
    }

Comment From: rstoyanchev

Yes that should be fine. Generally, when decoding input you can't reliably turn chunks into String since you might have split (multi-)character issues. However, that shouldn't be an issue when encoding output since the Encoder would not split characters like that.

Note that as of 5.2, DatBuffer has a toString() so you could simplify to:

private Function<DataBuffer, DataBuffer> writeToLog() {
    return dataBuffer -> {
        log(dataBuffer.toString(StandardCharsets.UTF_8));
        return dataBuffer;
    };
}

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: spring-projects-issues

Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.

Comment From: toxakktl

@rstoyanchev

public class SigningClientHttpRequestDecorator extends ClientHttpRequestDecorator {

    public SigningClientHttpRequestDecorator(ClientHttpRequest delegate) {
        super(delegate);
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
        Mono<DataBuffer> buffer = Mono.from(body);
        return super.writeWith(buffer.doOnNext(dataBuffer -> {
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                Channels.newChannel(baos).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
                Instant now = Instant.now();
                String encodedSign = SignUtil.signRequest(now, baos.toByteArray());
                super.getHeaders().set("testHeader", encodedSign);
            } catch (Exception e) {
                e.printStackTrace();
                throw new BlackboxResponseFilterException("Error applying filter to response");
            }
        }));
    }
}
public class SignClientHttpConnectorDecorator implements ClientHttpConnector {

    private final ClientHttpConnector delegate;

    public SignClientHttpConnectorDecorator(ClientHttpConnector delegate) {
        this.delegate = delegate;
    }

    @Override
    public Mono<ClientHttpResponse> connect(HttpMethod httpMethod,
                                            URI uri,
                                            Function<? super ClientHttpRequest, Mono<Void>> callback) {
        return this.delegate.connect(httpMethod, uri, clientHttpRequest -> callback.apply(new SigningClientHttpRequestDecorator(clientHttpRequest)));
    }
}

I am trying to set custom header, but super.getHeaders() returns empty result and they are read only, how can I set headers ? Here is how I set connector

HttpClient client = HttpClient.create();
//do some ssl stuff with client
ReactorClientHttpConnector connector = new ReactorClientHttpConnector(client);
        return WebClient.builder().clientConnector(new SignClientHttpConnectorDecorator(connector)).build();