Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e19ecf3
feat(okhttp): add telemetry interceptor
buongarzoni Mar 23, 2026
e141e7e
build(okhttp): update dependencies
buongarzoni Mar 24, 2026
ab26f42
chore(okhttp): add readme
buongarzoni Mar 24, 2026
ed9af20
chore(okhttp): fix lint
buongarzoni Mar 24, 2026
4f2e019
fix(okhttp): isolate NetworkTelemetryRecorder failures in interceptor
buongarzoni Apr 20, 2026
e7dd96f
build(okhttp): remove hardcoded version to inherit from root
buongarzoni Apr 20, 2026
ed91c57
build(okhttp): remove redundant plugins and repositories blocks
buongarzoni Apr 20, 2026
28807a1
fix(okhttp): strip query params from recorded URLs by default to prev…
buongarzoni Apr 27, 2026
02c7ea0
fix(okhttp): strip query params from recorded URLs by default to prev…
buongarzoni Apr 27, 2026
fd321cb
fix(okhttp): lint error, decrease line length
buongarzoni Apr 27, 2026
b4db532
fix(okhttp): attribute sanitizer exceptions to urlSanitizer in logs
buongarzoni May 1, 2026
ce27e7b
fix(okhttp): strip credentials and fragment from URLs in default sani…
buongarzoni May 1, 2026
2c505c1
fix(okhttp): replace JUL logger with SLF4J to match SDK conventions
buongarzoni May 1, 2026
3c4f505
build(okhttp): remove redundant mockito-core declaration
buongarzoni May 1, 2026
1d7dab4
docs(okhttp): add rollbar-java to installation snippet
buongarzoni May 1, 2026
f14a9eb
fix(okhttp): lint line length
buongarzoni May 1, 2026
0d75c63
style(okhttp): make test class public and use 2-space indentation
buongarzoni May 1, 2026
5835a82
style: add missing colon prefix to rollbar-okhttp in settings.gradle.kts
buongarzoni May 1, 2026
8af01d0
docs(okhttp): update sanitizer docs to list all stripped URL components
buongarzoni May 1, 2026
be12a54
fix(okhttp): replace java.util.function.Function with custom UrlSanit…
buongarzoni May 2, 2026
106f26d
fix(okhttp): remove incorrect group override so module publishes as c…
buongarzoni May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions rollbar-okhttp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Rollbar OkHttp Integration

This module provides an [OkHttp Interceptor](https://square.github.io/okhttp/features/interceptors/) that automatically captures network telemetry for the Rollbar Java SDK.

It records:

- **Network telemetry events** for HTTP responses with status code `>= 400` (client and server errors).
- **Error events** for connection failures, timeouts, and other I/O exceptions.

## Installation

### Gradle (Kotlin DSL)

```kotlin
dependencies {
implementation("com.rollbar:rollbar-java:<version>")
implementation("com.rollbar:rollbar-okhttp:<version>")
implementation("com.squareup.okhttp3:okhttp:<okhttp-version>")
}
```

### Gradle (Groovy)

```groovy
dependencies {
implementation 'com.rollbar:rollbar-java:<version>'
implementation 'com.rollbar:rollbar-okhttp:<version>'
implementation 'com.squareup.okhttp3:okhttp:<okhttp-version>'
}
```

## Usage
Comment thread
buongarzoni marked this conversation as resolved.

### 1. Implement `NetworkTelemetryRecorder`

```java
NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() {
@Override
public void recordNetworkEvent(Level level, String method, String url, String statusCode) {
// url has userinfo, query parameters, and fragment stripped by default
// (see Security section below)
rollbar.recordNetworkEventFor(level, method, url, statusCode);
}

@Override
public void recordErrorEvent(Exception exception) {
rollbar.log(exception);
}
};
```

### 2. Add the interceptor to your OkHttpClient

```java
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(recorder))
.build();
```

### 3. Make requests as usual

```java
Request request = new Request.Builder()
.url("https://api.example.com/data")
.build();

Response response = client.newCall(request).execute();
```

The interceptor will automatically record telemetry events to Rollbar without interfering with the request/response flow.

## Behavior

| Scenario | Action |
|-----------------------------------|---------------------------------------------------------|
| Recorder is `null` | No telemetry or log is recorded |
| Response status `< 400` | No telemetry recorded, response returned normally |
| Response status `>= 400` | Records a network telemetry event with `Level.CRITICAL` |
| Connection failure / timeout | Records an error event, then rethrows the `IOException` |

## Security

URLs can carry sensitive data in several components. To prevent accidental leakage to Rollbar, the interceptor **strips userinfo (basic-auth credentials), query parameters, and the fragment by default** before passing the URL to `NetworkTelemetryRecorder`.

For example, a request to `https://user:secret@api.example.com/charge?token=sk_live_secret#section` will be recorded as `https://api.example.com/charge`.

If your URLs do not contain sensitive query parameters and you need them for debugging, you can opt in to the full URL by supplying a custom sanitizer:

```java
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new RollbarOkHttpInterceptor(recorder, HttpUrl::toString))
.build();
```

When using a custom sanitizer, you are responsible for ensuring that sensitive query parameters are removed before the URL reaches Rollbar.
Comment thread
buongarzoni marked this conversation as resolved.
Comment on lines +87 to +95
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The opt-in example at README.md:91 uses HttpUrl::toString to re-enable the full URL, but the surrounding warning text at lines 87 and 95 only mentions "sensitive query parameters" — yet HttpUrl::toString simultaneously re-exposes userinfo (basic-auth credentials) and the fragment, which the Security paragraph at line 83 just promised the default strips. A reader whose URLs are query-param-clean but contain basic-auth credentials will follow the example and silently leak those credentials to Rollbar. Either change the example to a sanitizer that only opts into query (e.g. url -> url.newBuilder().username("").password("").fragment(null).build().toString()), or expand both warnings to enumerate all three components HttpUrl::toString re-exposes.

Extended reasoning...

What is wrong

The Security section frames URL sanitization as a defense for three sensitive URL components and the default sanitizer at RollbarOkHttpInterceptor.java:21-29 correctly strips all of them: username(""), password(""), query(null), fragment(null). README.md:83 documents this faithfully, listing userinfo (basic-auth credentials), query parameters, and the fragment. README.md:85 even illustrates with a URL that contains all three.

But the immediately following opt-in subsection narrows the framing back to one component:

  • Line 87: "If your URLs do not contain sensitive query parameters and you need them for debugging, you can opt in to the full URL by supplying a custom sanitizer:"
  • Line 91: example uses HttpUrl::toString
  • Line 95: "When using a custom sanitizer, you are responsible for ensuring that sensitive query parameters are removed before the URL reaches Rollbar."

HttpUrl::toString is OkHttp's canonical, fully-unredacted URL serialization — it preserves all three components the previous paragraph just promised the default strips, not just query parameters.

Step-by-step proof of the silent credential leak

  1. Reader skims the Security section and confirms their app does not put sensitive data in query strings.
  2. Reader follows line 87's suggestion and copies the line 91 example: new RollbarOkHttpInterceptor(recorder, HttpUrl::toString).
  3. Some endpoint in their app issues a request to https://alice:hunter2@api.example.com/charge (URL-embedded basic-auth — uncommon but legal and still seen in some legacy/internal integrations).
  4. The server returns 500. The interceptor calls urlSanitizer.sanitize(request.url()) which is now HttpUrl::toString.
  5. HttpUrl.toString() returns https://alice:hunter2@api.example.com/charge verbatim, userinfo intact.
  6. The README's recommended recorder forwards url straight to rollbar.recordNetworkEventFor(...). Basic-auth credentials are now stored in Rollbar.

The reader was never warned about userinfo or fragments because lines 87 and 95 only flag query parameters.

Why this PR is the right place to fix it

Commit 8af01d0 (docs(okhttp): update sanitizer docs to list all stripped URL components) already aligned the Security paragraph (line 83) and inline comment with the broadened sanitizer, but the opt-in paragraph and closing caveat were missed in that pass. Bringing them into alignment is a one-paragraph wording change that closes the gap before the docs are released.

Suggested fix — either of these:

  • (a) Change the example to retain credential/fragment stripping while opting into query: url -> url.newBuilder().username("").password("").fragment(null).build().toString(). This matches the framing the surrounding text already implies.
  • (b) Expand lines 87 and 95 to enumerate all three components and explicitly note that HttpUrl::toString re-exposes userinfo and the fragment in addition to query parameters.

Severity rationale

Docs-only, opt-in is deliberate, and URL-embedded basic-auth is rare in modern API usage — so this is a nit, not a normal-severity bug. It is still worth fixing in this same PR because (1) the section is explicitly framed as security, (2) the inconsistency with line 83 is internal to the same paragraph, and (3) the fix is trivial.

13 changes: 13 additions & 0 deletions rollbar-okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
dependencies {
testImplementation(platform("org.junit:junit-bom:5.14.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
Comment thread
buongarzoni marked this conversation as resolved.
api(project(":rollbar-api"))
api("org.slf4j:slf4j-api:1.7.25")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.rollbar.okhttp;

import com.rollbar.api.payload.data.Level;

/**
* Records network telemetry events and errors for HTTP requests.
*/
public interface NetworkTelemetryRecorder {
/**
* Records a completed network request as a telemetry event.
*
* @param level the severity level to attach to the telemetry event
* @param method the HTTP method (e.g. GET, POST)
* @param url the request URL with userinfo (basic-auth credentials), query parameters,
* and fragment stripped by default; supply a custom sanitizer to
* {@link RollbarOkHttpInterceptor} to change this behavior
* @param statusCode the HTTP response status code as a string (e.g. "200", "404")
*/
void recordNetworkEvent(Level level, String method, String url, String statusCode);

/**
* Records a network error event when an HTTP request fails with an exception.
*
* @param exception the exception thrown during the request
*/
void recordErrorEvent(Exception exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.rollbar.okhttp;

import com.rollbar.api.payload.data.Level;

import java.io.IOException;
import java.util.Objects;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RollbarOkHttpInterceptor implements Interceptor {

private static final Logger LOGGER = LoggerFactory.getLogger(RollbarOkHttpInterceptor.class);

private static final UrlSanitizer DEFAULT_URL_SANITIZER =
url -> url
.newBuilder()
.username("")
.password("")
.query(null)
.fragment(null)
.build()
.toString();

private final NetworkTelemetryRecorder recorder;
private final UrlSanitizer urlSanitizer;

public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) {
this(recorder, DEFAULT_URL_SANITIZER);
}

public RollbarOkHttpInterceptor(
NetworkTelemetryRecorder recorder,
UrlSanitizer urlSanitizer) {
this.recorder = recorder;
this.urlSanitizer = Objects.requireNonNull(urlSanitizer, "urlSanitizer must not be null");
}

@NotNull
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();

try {
Response response = chain.proceed(request);

if (response.code() >= 400 && recorder != null) {
String sanitizedUrl;
try {
sanitizedUrl = urlSanitizer.sanitize(request.url());
} catch (Exception sanitizerException) {
LOGGER.warn("urlSanitizer threw an exception; "
+ "suppressing to preserve the interceptor contract.", sanitizerException);
return response;
}
try {
recorder.recordNetworkEvent(
Level.CRITICAL,
request.method(),
sanitizedUrl,
String.valueOf(response.code()));
Comment thread
claude[bot] marked this conversation as resolved.
} catch (Exception recorderException) {
LOGGER.warn("NetworkTelemetryRecorder.recordNetworkEvent threw an exception; "
+ "suppressing to preserve the interceptor contract.", recorderException);
}
Comment thread
buongarzoni marked this conversation as resolved.
}

return response;

} catch (IOException e) {
if (recorder != null) {
try {
recorder.recordErrorEvent(e);
} catch (Exception recorderException) {
LOGGER.warn("NetworkTelemetryRecorder.recordErrorEvent threw an exception; "
+ "suppressing to preserve the original IOException.", recorderException);
}
}

throw e;
Comment thread
claude[bot] marked this conversation as resolved.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.rollbar.okhttp;

import okhttp3.HttpUrl;

@FunctionalInterface
public interface UrlSanitizer {
String sanitize(HttpUrl url);
}
Loading
Loading