Skip to content

RUM-15591: Expose request/response payloads on HttpRequestInfo/HttpResponseInfo - #3714

Draft
kikoveiga wants to merge 1 commit into
developfrom
kikoveiga/rum-15591/http-body-access
Draft

RUM-15591: Expose request/response payloads on HttpRequestInfo/HttpResponseInfo#3714
kikoveiga wants to merge 1 commit into
developfrom
kikoveiga/rum-15591/http-body-access

Conversation

@kikoveiga

Copy link
Copy Markdown
Contributor

What does this PR do?

Add peekBody(maxBytes) to both core interfaces, returning an HttpBodySnapshot that carries the bytes, the declared content type and an isTruncated flag. Reading is lazy and non-consuming: nothing is copied unless the method is called, and the request stays sendable / the response stays readable. The default cap is 512 KB, matching dd-sdk-ios#3019.

OkHttp implements both sides while Cronet returns null, because it streams payloads directly between the application and the network stack, so exposing them would require buffering a copy of every request whether or not a snapshot is ever asked for. That trade-off is left to a follow-up.

Motivation

The deprecated RumResourceAttributesProvider overload handed customers the raw okhttp3.Request/Response, so they could read the payloads themselves. The new library-agnostic instrumentation didn't have this option.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

…tpResponseInfo

The deprecated RumResourceAttributesProvider overload handed customers the raw
okhttp3.Request/Response, so they could read the payloads themselves. The
library-agnostic HttpRequestInfo/HttpResponseInfo replacement exposed no way to
do that (#3365).

Add peekBody(maxBytes) to both core interfaces, returning an HttpBodySnapshot
that carries the bytes, the declared content type and an isTruncated flag.
Reading is lazy and non-consuming: nothing is copied unless the method is
called, and the request stays sendable / the response stays readable. The
default cap is 512 KB, matching dd-sdk-ios#3019.

OkHttp implements both sides. Requests are written through a truncating sink so
a large upload never allocates more than the requested maximum, and one-shot
and duplex bodies are skipped because writing them would break the request.
Responses go through Response.peekBody, skipping streams and WebSockets whose
payloads would never complete on their own.

Cronet returns null on both, documented at the override: it streams payloads
directly between the application and the network stack, so exposing them would
require buffering a copy of every request whether or not a snapshot is ever
asked for. That trade-off is left to a follow-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented Aug 14, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 82.76%
Overall Coverage: 71.82% (+0.09%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d8ec45a | Docs | Datadog PR Page | Give us feedback!

import java.util.Locale

/**
* An immutable, size-capped copy of an HTTP request or response payload.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's immutable - maybe better to declare as data class

private const val HASH_MULTIPLIER = 31
private const val CHARSET_PARAMETER = "charset"

private fun resolveCharset(contentType: String?): Charset {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am bit afraid this method might be silently complex from the memory perspective because of transformations.

For instance - firstOrNull { it.lowercase(Locale.US) } - will create a String object for each element of array.
?.map { it.trim() } - as well
and so on.

so in case if this method would be called multiple times in a short period - it could create a pressure to the GC.

I am not sure that you could completley avoid that, but manual iteration over the string, might be cheaper. Good test coverage would be required as well for all supported HTTP header format

@abrooksv abrooksv Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startsWith(.., ignoreCase = true) could help here, it delegates down to String.regionMatches, which does iteration without us blowing up the method complexity (still pay the trim cost though)

Then you can defer the lowercasing cost only when a match is found

"$CHARSET_PARAMETER=" should probably also be a const so you don't pay the cost of building the search string sequence iteration

* every upload as it flows past, at a memory cost paid by every request whether or not a
* snapshot is ever asked for.
*/
override fun peekBody(maxBytes: Long): HttpBodySnapshot? = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this api could not be supported for both implementation it's better to move peekBody only to OkHttpRequestInfo and add extension method peakBody that could check if given HttpRequestInfo is OkHttpRequestInfo then cast and call, otherwise return null.

* as it streams past, at a memory cost paid by every request whether or not a snapshot is ever
* asked for.
*/
override fun peekBody(maxBytes: Long): HttpBodySnapshot? = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here:
if this api could not be supported for both implementation it's better to move peekBody only to OkHttpRequestInfo and add extension method peakBody that could check if given HttpRequestInfo is OkHttpRequestInfo then cast and call, otherwise return null.

override fun newBuilder() = OkHttpRequestInfoBuilder(originalRequest.newBuilder())

internal companion object {
internal const val ERROR_PEEK_REQUEST_BODY = "Unable to peek request body."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if companion object is internal should we make ERROR_PEEK_REQUEST_BODY internal as well?

}
}

override fun flush() = Unit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like flush/close methods should somehow affect internal buffer? like cleaning it ? Otherwise there is a risk for memory leaks


// A snapshot is backed by a ByteArray, which cannot hold more than Int.MAX_VALUE bytes.
// One byte is kept in reserve for the truncation probe in peekBody.
private const val MAX_SNAPSHOT_BYTES: Long = Int.MAX_VALUE.toLong() - 1L

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you that it's safe to let buffer be limited with Int.MAX_VALUE? ~2GB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants