Okhttp3.14 Stream closed - okhttp

I have some usage issue about okhttp in 3.14.9 release
if i want add LoggingInterceptor for each request, how can i get response body, which can only consume once?
And follow is my attemp
public class LoggingRequestInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
log.debug(
"{}, {}, {}, {}, {}, {}, {}",
request.url(),
request.method(),
JSONUtil.toJsonStr(request.body()),
request.headers(),
dup.body() == null ? null : dup.body().string());
return response;
}
}
It will throw exception of stream closed, how to fix it?

Use peekBody for this
val client = OkHttpClient.Builder()
.addInterceptor {
val response = it.proceed(it.request())
println(response.peekBody(1000000).string())
response
}
.build()

I do research for this issue. We can use buffer, which is in RequestBody-source-getBuffer.
Working code is below:
public String getResponseBody(Response response) {
try {
ResponseBody responseBody = response.body();
if (!ObjectUtil.isNull(responseBody)) {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.getBuffer();
return buffer.clone().readString(UTF8);
}
} catch (IOException e) {
log.error("get response body failed: ", e);
}
return null;
}

From ernest-kiwele:
Using a try with a resources block with the response causes this "closed" problem when the response body is read outside of the try block:
try (Response response = client.newCall(request.build()).execute()) {
return response;
} //response.close() called implicitly by the JVM
The fix is to restructure the code to only use the response within the try block.

Related

Handle error on HttpServletResponse, when OutputStream is used

I have an API method that need send response using HttpServletResponse.getOutputStream(). Is possible to return error response with some http status code when error occurs when outputstream is used like that ? In below code i'm getting Cannot call sendError() after the response has been committed exception and response not completed response but with status 200. I think that because stream is opened and something was written already... Any workaround for this situation?
#GetMapping(path = "/test")
public void test(HttpServletResponse resp) {
try (
OutputStream out = resp.getOutputStream();
) {
// writing to out...
// writing to out...
// ...
// some error occurs
// ...
} catch(IOException e) {
// Cannot call sendError() after the response has been committed
resp.sendError(500, "something wrong");
}
}

How to create spring cloud gateway filter with synchronous http request

I'm creating a GateWayFilter to authenticate a request with a ticket.
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//If you want to build a "pre" filter you need to manipulate the
//request before calling chain.filter
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpRequest.Builder builder = serverHttpRequest.mutate();
//use builder to manipulate the request
List<String> tickets = serverHttpRequest.getQueryParams().get(TICKET);
if (CollectionUtils.isEmpty(tickets)) {
return onError(exchange);
}
String ticket = tickets.get(0);
// todo use MONO?
// todo add cache
final ApiCaller apiCaller;
try {
apiCaller = HttpRequestUtil.getObject(
"http://localhost:9001/authapi/authentication/ticket/" + ticket, ApiCaller.class);
} catch (IOException e) {
log.warn("failed authenticate ticket: {}", ticket, e);
return onError(exchange);
}
if (apiCaller == null || StringUtils.isBlank(apiCaller.getLipCode())) {
log.info("not valid ticket: {}", ticket);
return onError(exchange);
}
ApiCallerUtil.addApiCaller(builder, apiCaller);
return chain.filter(exchange.mutate().request(builder.build()).build());
}
the problem is I use a blocking http request in the method,
Question 1: Is there any disadvantage of using the blocking HTTP request?
Question 2:How can I chang to reactor http request to get the ApiCaller Information?

Handle Spring WebFlux WebClient timeout in Kotlin

I am doing a get http call with Spring WebFlux WebClient (Boot 2.4.3) in Kotlin (1.4.30). When request times out it fails with exception but instead I'd like to return a default value. I see references to onError, onStatus etc. used after retrieve() but they don't seem to be available in my case (only body, toEntity, awaitExchange)
The call:
suspend fun conversation(id: String): Conversation =
client.get().uri("/conversation/{id}", id).retrieve().awaitBody()
WebClient configuration with connect and read timeouts:
fun webClient(url: String, connectTimeout: Int, readTimeout: Long, objectMapper: ObjectMapper): WebClient =
WebClient.builder()
.baseUrl(url)
.exchangeStrategies(
ExchangeStrategies.builder()
.codecs { configurer -> configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) }
.build())
.clientConnector(
ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
.doOnConnected { connection ->
connection.addHandlerLast(ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))
}))
.build()
Response model:
#JsonIgnoreProperties(ignoreUnknown = true)
data class Conversation(
val replyTimestamp: Map<String, String>,
)
How can I return default response (conversation with empty map) in case of timeout instead of failing with an exception?
Update:
I tried suggestion of JArgente below: updated the call with awaitExchange and set valid WireMock response with delay (1010 ms) that is longer that timeout (1000 ms).
Result is still ReadTimeoutException so looking at http status code does not help in this case.
private val defaultConversation = Conversation(emptyMap())
suspend fun conversation(id: String): Conversation =
client.get()
.uri("/conversation/{id}", id)
.awaitExchange {
response -> if (response.statusCode() == HttpStatus.OK) response.awaitBody() else defaultConversation
}
Response:
{
"replyTimestamp": {
"1": "2021-02-23T15:30:28.753Z",
"2": "2021-02-23T16:30:28.753Z"
}
}
Mock config for it:
{
"mappings":
[
{
"priority": 1,
"request": {
"method": "GET",
"urlPathPattern": "/conversation/1"
},
"response": {
"status": 200,
"fixedDelayMilliseconds": 1010,
"headers": {
"content-type": "application/json;charset=utf-8"
},
"bodyFileName": "conversation1.json"
}
}
]
}
You are getting an exception because in your method, you are expecting to get a response of type Conversation, but because you are receiving an error, the body is different.
The way you should handle the response, in this case, should be first, to look at the HTTP status code and then convert the body accordingly.
Here is an example from spring.io
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html
In your case, when you receive the status code of the error you should create a new empty Conversation and return it
val entity = client.get()
.uri("/conversation/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Conversation>()
}
else if (response.statusCode().is4xxClientError) {
return response.awaitBody<ErrorContainer>()
}
else {
throw response.createExceptionAndAwait()
}
}
As per Martin's suggestion ended up just wrapping call in a try/catch:
suspend inline fun <reified T : Any> WebClient.ResponseSpec.tryAwaitBodyOrElseLogged(default: T, log: Logger) : T =
try {
awaitBody()
} catch (e: Exception) {
log.warn("Remote request failed, returning default value ($default)", e)
default
}
private val log = LoggerFactory.getLogger(this::class.java)
private val default = Conversation(emptyMap())
suspend fun conversation(id: String): Conversation =
client.get()
.uri("/conversation/{id}", id)
.retrieve()
.tryAwaitBodyOrElseLogged(default, log)
I thought there is some idiomatic way but this works fine.

Retrotif2 + RxJava sending POST request failed

I want to send POST request with Retrofit + RxJava, but it is failing and I don't know the reason. In one activity it's working, in another - don't want to work:
private void sendMerchantInfo() {
try {
String advertiserOriginalDeepLink = "https://mywebsite.com/main-1?param1=value1&param2=value2";
String urlGetParams = LinkParser.getUrlGETParams(advertiserOriginalDeepLink);
Map<Object, Object> merchantInfo = LinkParser.parseUrlGetParams(urlGetParams);
String merchantInfoJson = new Gson().toJson(merchantInfo); //{"param1":"value1","param2":"value2"}
String url = "https://api.endpoint.com/v1/system/merchant/process";
userService = this.serviceGenerator.createService(UserService.class, true);
final Observable observable = userService.sendUserInfo(
url, new RetrofitMapBody(merchantInfo))
.doOnNext(new Consumer<ResponseBody>() {
#Override
public void accept(ResponseBody responseBody) throws Exception {
//handle 200 OK.
}
})
.onErrorResumeNext((ObservableSource<? extends ResponseBody>) v ->
Crashlytics.log("Send user info attempt failed."))
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
addDisposable(observable.subscribe());
}
} catch (Exception exception) {
Crashlytics.log("Send user info attempt failed. " + exception.getMessage());
}
}
I suspect that problem in this part, I am trying to send request in OnCreate() method:
.subscribeOn(Schedulers.from(threadExecutor))
.observeOn(postExecutionThread.getScheduler());
Tried to use this, but no effect:
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
What I am doing wrong? It always call onErrorResumeNext() It's probably something with threads because one time I got exception: networkonmainthreadexception. Please help.
Try using RxJava2 Adapter, it will save you a lot!
Step 1: Retrofit client setup
private Retrofit getRetrofitClient() {
return new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //option 1
.addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.newThread())) //option 2
.build();
}
Step 2: APIService interface (Example)
#GET("endpoint")
Single<ResponseModel> fetch();
Step 3: Usage
Single<ResponseModel> fetch() {
return getRetrofitClient()
.create(APIService.class)
.fetch();
}
Any non-2xx HTTP response will be wrapped in HttpException from which you can extract the status code, the status message and the full HTTP response.
Any connection errors will be wrapped in IOException
And that is all you need to do to wrap your network call in any RxJava stream.

How to log request and response bodies in Spring WebFlux

I want to have centralised logging for requests and responses in my REST API on Spring WebFlux with Kotlin. So far I've tried this approaches
#Bean
fun apiRouter() = router {
(accept(MediaType.APPLICATION_JSON) and "/api").nest {
"/user".nest {
GET("/", userHandler::listUsers)
POST("/{userId}", userHandler::updateUser)
}
}
}.filter { request, next ->
logger.info { "Processing request $request with body ${request.bodyToMono<String>()}" }
next.handle(request).doOnSuccess { logger.info { "Handling with response $it" } }
}
Here request method and path log successfully but the body is Mono, so how should I log it? Should it be the other way around and I have to subscribe on request body Mono and log it in the callback?
Another problem is that ServerResponse interface here doesn't have access to the response body. How can I get it here?
Another approach I've tried is using WebFilter
#Bean
fun loggingFilter(): WebFilter =
WebFilter { exchange, chain ->
val request = exchange.request
logger.info { "Processing request method=${request.method} path=${request.path.pathWithinApplication()} params=[${request.queryParams}] body=[${request.body}]" }
val result = chain.filter(exchange)
logger.info { "Handling with response ${exchange.response}" }
return#WebFilter result
}
Same problem here: request body is Flux and no response body.
Is there a way to access full request and response for logging from some filters? What don't I understand?
This is more or less similar to the situation in Spring MVC.
In Spring MVC, you can use a AbstractRequestLoggingFilter filter and ContentCachingRequestWrapper and/or ContentCachingResponseWrapper. Many tradeoffs here:
if you'd like to access servlet request attributes, you need to actually read and parse the request body
logging the request body means buffering the request body, which can use a significant amount of memory
if you'd like to access the response body, you need to wrap the response and buffer the response body as it's being written, for later retrieval
ContentCaching*Wrapper classes don't exist in WebFlux but you could create similar ones. But keep in mind other points here:
buffering data in memory somehow goes against the reactive stack, since we're trying there to be very efficient with the available resources
you should not tamper with the actual flow of data and flush more/less often than expected, otherwise you'd risk breaking streaming uses cases
at that level, you only have access to DataBuffer instances, which are (roughly) memory-efficient byte arrays. Those belong to buffer pools and are recycled for other exchanges. If those aren't properly retained/released, memory leaks are created (and buffering data for later consumption certainly fits that scenario)
again at that level, it's only bytes and you don't have access to any codec to parse the HTTP body. I'd forget about buffering the content if it's not human-readable in the first place
Other answers to your question:
yes, the WebFilter is probably the best approach
no, you shouldn't subscribe to the request body otherwise you'd consume data that the handler won't be able to read; you can flatMap on the request and buffer data in doOn operators
wrapping the response should give you access to the response body as it's being written; don't forget about memory leaks, though
I didn't find a good way to log request/response bodies, but if you are just interested in meta data then you can do it like follows.
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
#Component
class LoggingFilter(val requestLogger: RequestLogger, val requestIdFactory: RequestIdFactory) : WebFilter {
val logger = logger()
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info(requestLogger.getRequestMessage(exchange))
val filter = chain.filter(exchange)
exchange.response.beforeCommit {
logger.info(requestLogger.getResponseMessage(exchange))
Mono.empty()
}
return filter
}
}
#Component
class RequestLogger {
fun getRequestMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val method = request.method
val path = request.uri.path
val acceptableMediaTypes = request.headers.accept
val contentType = request.headers.contentType
return ">>> $method $path ${HttpHeaders.ACCEPT}: $acceptableMediaTypes ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
fun getResponseMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val response = exchange.response
val method = request.method
val path = request.uri.path
val statusCode = getStatus(response)
val contentType = response.headers.contentType
return "<<< $method $path HTTP${statusCode.value()} ${statusCode.reasonPhrase} ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
private fun getStatus(response: ServerHttpResponse): HttpStatus =
try {
response.statusCode
} catch (ex: Exception) {
HttpStatus.CONTINUE
}
}
This is what I came up with for java.
public class RequestResponseLoggingFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest httpRequest = exchange.getRequest();
final String httpUrl = httpRequest.getURI().toString();
ServerHttpRequestDecorator loggingServerHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
String requestBody = "";
#Override
public Flux<DataBuffer> getBody() {
return super.getBody().doOnNext(dataBuffer -> {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
requestBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
commonLogger.info(LogMessage.builder()
.step(httpUrl)
.message("log incoming http request")
.stringPayload(requestBody)
.build());
} catch (IOException e) {
commonLogger.error(LogMessage.builder()
.step("log incoming request for " + httpUrl)
.message("fail to log incoming http request")
.errorType("IO exception")
.stringPayload(requestBody)
.build(), e);
}
});
}
};
ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
String responseBody = "";
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Mono<DataBuffer> buffer = Mono.from(body);
return super.writeWith(buffer.doOnNext(dataBuffer -> {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
responseBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
commonLogger.info(LogMessage.builder()
.step("log outgoing response for " + httpUrl)
.message("incoming http request")
.stringPayload(responseBody)
.build());
} catch (Exception e) {
commonLogger.error(LogMessage.builder()
.step("log outgoing response for " + httpUrl)
.message("fail to log http response")
.errorType("IO exception")
.stringPayload(responseBody)
.build(), e);
}
}));
}
};
return chain.filter(exchange.mutate().request(loggingServerHttpRequestDecorator).response(loggingServerHttpResponseDecorator).build());
}
}
You can actually enable DEBUG logging for Netty and Reactor-Netty related to see full picture of what's happening. You could play with the below and see what you want and don't. That was the best I could.
reactor.ipc.netty.channel.ChannelOperationsHandler: DEBUG
reactor.ipc.netty.http.server.HttpServer: DEBUG
reactor.ipc.netty.http.client: DEBUG
io.reactivex.netty.protocol.http.client: DEBUG
io.netty.handler: DEBUG
io.netty.handler.proxy.HttpProxyHandler: DEBUG
io.netty.handler.proxy.ProxyHandler: DEBUG
org.springframework.web.reactive.function.client: DEBUG
reactor.ipc.netty.channel: DEBUG
Since Spring Boot 2.2.x, Spring Webflux supports Kotlin coroutines. With coroutines, you can have the advantages of non-blocking calls without having to handle Mono and Flux wrapped objects. It adds extensions to ServerRequest and ServerResponse, adding methods like ServerRequest#awaitBody() and ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any). So you could rewrite you code like this:
#Bean
fun apiRouter() = coRouter {
(accept(MediaType.APPLICATION_JSON) and "/api").nest {
"/user".nest {
/* the handler methods now use ServerRequest and ServerResponse directly
you just need to add suspend before your function declaration:
suspend fun listUsers(ServerRequest req, ServerResponse res) */
GET("/", userHandler::listUsers)
POST("/{userId}", userHandler::updateUser)
}
}
// this filter will be applied to all routes built by this coRouter
filter { request, next ->
// using non-blocking request.awayBody<T>()
logger.info("Processing $request with body ${request.awaitBody<String>()}")
val res = next(request)
logger.info("Handling with Content-Type ${res.headers().contentType} and status code ${res.rawStatusCode()}")
res
}
}
In order to create a WebFilter Bean with coRoutines, I think you can use this CoroutineWebFilter interface (I haven't tested it, I don't know if it works).
I am pretty new to Spring WebFlux, and I don't know how to do it in Kotlin, but should be the same as in Java using WebFilter:
public class PayloadLoggingWebFilter implements WebFilter {
public static final ByteArrayOutputStream EMPTY_BYTE_ARRAY_OUTPUT_STREAM = new ByteArrayOutputStream(0);
private final Logger logger;
private final boolean encodeBytes;
public PayloadLoggingWebFilter(Logger logger) {
this(logger, false);
}
public PayloadLoggingWebFilter(Logger logger, boolean encodeBytes) {
this.logger = logger;
this.encodeBytes = encodeBytes;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (logger.isInfoEnabled()) {
return chain.filter(decorate(exchange));
} else {
return chain.filter(exchange);
}
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public Flux<DataBuffer> getBody() {
if (logger.isDebugEnabled()) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
return super.getBody().map(dataBuffer -> {
try {
Channels.newChannel(baos).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
} catch (IOException e) {
logger.error("Unable to log input request due to an error", e);
}
return dataBuffer;
}).doOnComplete(() -> flushLog(baos));
} else {
return super.getBody().doOnComplete(() -> flushLog(EMPTY_BYTE_ARRAY_OUTPUT_STREAM));
}
}
};
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return decorated;
}
private void flushLog(ByteArrayOutputStream baos) {
ServerHttpRequest request = super.getRequest();
if (logger.isInfoEnabled()) {
StringBuffer data = new StringBuffer();
data.append('[').append(request.getMethodValue())
.append("] '").append(String.valueOf(request.getURI()))
.append("' from ")
.append(
Optional.ofNullable(request.getRemoteAddress())
.map(addr -> addr.getHostString())
.orElse("null")
);
if (logger.isDebugEnabled()) {
data.append(" with payload [\n");
if (encodeBytes) {
data.append(new HexBinaryAdapter().marshal(baos.toByteArray()));
} else {
data.append(baos.toString());
}
data.append("\n]");
logger.debug(data.toString());
} else {
logger.info(data.toString());
}
}
}
};
}
}
Here some tests on this: github
I think this is what Brian Clozel (#brian-clozel) meant.
Here is the GitHub Repo with complete implementation to log both request and response body along with http headers for webflux/java based application...
What Brian said. In addition, logging request/response bodies don't make sense for reactive streaming. If you imagine the data flowing through a pipe as a stream, you don't have the full content at any time unless you buffer it, which defeats the whole point. For small request/response, you can get away with buffering, but then why use the reactive model (other than to impress your coworkers :-) )?
The only reason for logging request/response that I could conjure up is debugging, but with the reactive programming model, debugging method has to be modified too. Project Reactor doc has an excellent section on debugging that you can refer to: http://projectreactor.io/docs/core/snapshot/reference/#debugging
Assuming we are dealing with a simple JSON or XML response, if debug level for corresponding loggers is not sufficient for some reason, one can use string representation before transforming it to object:
Mono<Response> mono = WebClient.create()
.post()
.body(Mono.just(request), Request.class)
.retrieve()
.bodyToMono(String.class)
.doOnNext(this::sideEffectWithResponseAsString)
.map(this::transformToResponse);
the following are the side-effect and transformation methods:
private void sideEffectWithResponseAsString(String response) { ... }
private Response transformToResponse(String response) { /*use Jackson or JAXB*/ }
If your using controller instead of handler best way is aop with annotating you controller class with #Log annotation.And FYI this takes plain json object as request not mono.
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class Log
#Aspect
#Component
class LogAspect {
companion object {
val log = KLogging().logger
}
#Around("#annotation(Log)")
#Throws(Throwable::class)
fun logAround(joinPoint: ProceedingJoinPoint): Any? {
val start = System.currentTimeMillis()
val result = joinPoint.proceed()
return if (result is Mono<*>) result.doOnSuccess(getConsumer(joinPoint, start)) else result
}
fun getConsumer(joinPoint: ProceedingJoinPoint, start: Long): Consumer<Any>? {
return Consumer {
var response = ""
if (Objects.nonNull(it)) response = it.toString()
log.info(
"Enter: {}.{}() with argument[s] = {}",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args
)
log.info(
"Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args[0],
response, System.currentTimeMillis() - start
)
}
}
}
I think the appropriate thing to do here is to write the contents of each request to a file in an asynchronous manner (java.nio) and set up an interval that reads those request body files asynchrolusly and writes them to the log in a memory usage aware manner (atleast one file at a time but up too 100 mb at a time) and after logging them removes the files from disk.
Ivan Lymar's answer but in Kotlin:
import org.apache.commons.io.IOUtils
import org.reactivestreams.Publisher
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
import org.springframework.http.server.reactive.ServerHttpResponseDecorator
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.channels.Channels
#Component
class LoggingWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val httpRequest = exchange.request
val httpUrl = httpRequest.uri.toString()
val loggingServerHttpRequestDecorator: ServerHttpRequestDecorator =
object : ServerHttpRequestDecorator(exchange.request) {
var requestBody = ""
override fun getBody(): Flux<DataBuffer> {
return super.getBody().doOnNext { dataBuffer: DataBuffer ->
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
Channels.newChannel(byteArrayOutputStream)
.write(dataBuffer.asByteBuffer().asReadOnlyBuffer())
requestBody =
IOUtils.toString(
byteArrayOutputStream.toByteArray(),
"UTF-8"
)
log.info(
"Logging Request Filter: {} {}",
httpUrl,
requestBody
)
}
} catch (e: IOException) {
log.error(
"Logging Request Filter Error: {} {}",
httpUrl,
requestBody,
e
)
}
}
}
}
val loggingServerHttpResponseDecorator: ServerHttpResponseDecorator =
object : ServerHttpResponseDecorator(exchange.response) {
var responseBody = ""
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
val buffer: Mono<DataBuffer> = Mono.from(body)
return super.writeWith(
buffer.doOnNext { dataBuffer: DataBuffer ->
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
Channels.newChannel(byteArrayOutputStream)
.write(
dataBuffer
.asByteBuffer()
.asReadOnlyBuffer()
)
responseBody = IOUtils.toString(
byteArrayOutputStream.toByteArray(),
"UTF-8"
)
log.info(
"Logging Response Filter: {} {}",
httpUrl,
responseBody
)
}
} catch (e: Exception) {
log.error(
"Logging Response Filter Error: {} {}",
httpUrl,
responseBody,
e
)
}
}
)
}
}
return chain.filter(
exchange.mutate().request(loggingServerHttpRequestDecorator)
.response(loggingServerHttpResponseDecorator)
.build()
)
}
}

Resources