Attach request body for Spring WebFlux
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 ?
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.
@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).
👋 is there any news on this?
Hey @zeletrik thanks for checking in - but no news yet, we can't give an ETA right now
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.