sentry-java icon indicating copy to clipboard operation
sentry-java copied to clipboard

Attach request body for Spring WebFlux

Open nosiar opened this issue 2 years ago • 6 comments

Integration

sentry-spring-boot-jakarta

Java Version

17

Version

6.28.0

Steps to Reproduce

spring boot 3.0

application.properties

sentry.send-default-pii=true
sentry.max-request-body-size=always

Expected Result

request body should be sent

Actual Result

On SentrySpringFilter, it seems like CachedBodyHttpServletRequest is created and request body is extracted by RequestPayloadExtractor

but on SentryWebFilter, i can't find such process. Is there any way to send request body with SentryWebFilter ?

nosiar avatar Aug 23 '23 13:08 nosiar

Hi @nosiar, Thanks for reaching out to us.

AFAIK you are correct, sending the request body is not yet implemented in the Webflux integration. Will bring this up internally and let you know as soon as we have an update on this.

lbloder avatar Aug 30 '23 15:08 lbloder

@nosiar thanks for opening this issue. Sending the request body has not yet been implemented for WebFlux. We can use this issue for tracking the feature in our backlog. Can't say when we'll get around to implementing this though.

When implementing we should check maxRequestBodySize and compare to actual request body size (not sure where to get that from yet) then retrieve from serverWebExchange.getRequest().getBody() and put that on the events request.data.

If you would like to have this feature before we implement it you could take a look at SentrySpringFilter to see how it's done for WebMVC (it checks config and content length first; caches the body and adds an EventProcessor on the scope that then attaches the body to request.data of the event).

adinauer avatar Sep 04 '23 07:09 adinauer

👋 is there any news on this?

zeletrik avatar Jul 23 '24 12:07 zeletrik

Hey @zeletrik thanks for checking in - but no news yet, we can't give an ETA right now

kahest avatar Jul 25 '24 14:07 kahest


import io.netty.buffer.ByteBufAllocator
import io.sentry.EventProcessor
import io.sentry.Hint
import io.sentry.IHub
import io.sentry.SentryEvent
import io.sentry.SentryOptions.RequestSize
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DataBufferUtils
import org.springframework.core.io.buffer.NettyDataBufferFactory
import org.springframework.http.server.reactive.ServerHttpRequest
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
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.nio.charset.StandardCharsets

class SentryRequestBodyFilter(
    private val hub: IHub,
) : WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> =
        Mono.just(exchange)
            .filter { isQualifiedForCaching(it.request, hub.options.maxRequestBodySize) }
            .flatMap { DataBufferUtils.join(it.request.body) }
            .map { buffer ->
                exchange.mutate()
                    .request(cacheRequest(exchange.request, buffer))
                    .build()
            }
            .defaultIfEmpty(exchange)
            .flatMap {
                chain.filter(it)
                    .doFirst {
                        it.getAttribute<IHub>(SENTRY_HUB_KEY)
                            ?.configureScope { scope ->
                                scope.addEventProcessor(RequestBodyExtractingEventProcessor(it.request))
                            }
                    }
            }

    private fun isQualifiedForCaching(
        request: ServerHttpRequest,
        maxRequestBodySize: RequestSize
    ): Boolean {
        val contentLength = request.headers.contentLength

        return (
            contentLength != -1L && (
                (maxRequestBodySize == RequestSize.SMALL && contentLength < 1000) ||
                    (maxRequestBodySize == RequestSize.MEDIUM && contentLength < 10000) ||
                    (maxRequestBodySize == RequestSize.ALWAYS)
                )
            )
    }

    private fun cacheRequest(request: ServerHttpRequest, buffer: DataBuffer): ServerHttpRequest {
        val bytes = ByteArray(buffer.readableByteCount())
        buffer.read(bytes)
        DataBufferUtils.release(buffer)

        return CachedBodyServerHttpRequest(request, bytes)
    }

    class CachedBodyServerHttpRequest(
        delegate: ServerHttpRequest,
        val cachedBody: ByteArray,
    ) : ServerHttpRequestDecorator(delegate) {
        override fun getBody(): Flux<DataBuffer> {
            return DataBufferUtils.read(
                ByteArrayResource(cachedBody),
                NettyDataBufferFactory(ByteBufAllocator.DEFAULT),
                cachedBody.size
            )
        }
    }

    class RequestBodyExtractingEventProcessor(
        private val request: ServerHttpRequest
    ) : EventProcessor {
        override fun process(event: SentryEvent, hint: Hint): SentryEvent {
            if (event.request != null) {
                event.request!!.data = extract(request)
            }
            return event
        }

        private fun extract(request: ServerHttpRequest): String? {
            if (request is CachedBodyServerHttpRequest) {
                return String(request.cachedBody, StandardCharsets.UTF_8)
            }
            return null
        }
    }

    companion object {
        const val SENTRY_HUB_KEY = "sentry-hub"
    }
}

Here is my filter for attaching request body.

lee-jinhwan avatar Aug 16 '24 12:08 lee-jinhwan