From e19ecf38baf3eafbb3156a20193cf5c6b09529f3 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 23 Mar 2026 18:53:04 -0300 Subject: [PATCH 01/21] feat(okhttp): add telemetry interceptor --- rollbar-okhttp/build.gradle.kts | 24 +++ .../okhttp/NetworkTelemetryRecorder.java | 8 + .../okhttp/RollbarOkHttpInterceptor.java | 39 +++++ .../okhttp/RollbarOkHttpInterceptorTest.java | 148 ++++++++++++++++++ settings.gradle.kts | 1 + 5 files changed, 220 insertions(+) create mode 100644 rollbar-okhttp/build.gradle.kts create mode 100644 rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java create mode 100644 rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java create mode 100644 rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts new file mode 100644 index 00000000..d694a634 --- /dev/null +++ b/rollbar-okhttp/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("java") +} + +group = "com.rollbar.okhttp" +version = "2.2.0" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.10.0")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testImplementation("com.squareup.okhttp3:mockwebserver:4.9.2") + testImplementation("org.mockito:mockito-core:5.23.0") + implementation("com.squareup.okhttp3:okhttp:4.9.2") + api(project(":rollbar-api")) +} + +tasks.test { + useJUnitPlatform() +} diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java new file mode 100644 index 00000000..7360ec1c --- /dev/null +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java @@ -0,0 +1,8 @@ +package com.rollbar.okhttp; + +import com.rollbar.api.payload.data.Level; + +public interface NetworkTelemetryRecorder { + void recordNetworkEvent(Level level, String method, String url, String statusCode); + void recordErrorEvent(Exception exception); +} diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java new file mode 100644 index 00000000..209fd174 --- /dev/null +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -0,0 +1,39 @@ +package com.rollbar.okhttp; + +import com.rollbar.api.payload.data.Level; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +public class RollbarOkHttpInterceptor implements Interceptor { + + private final NetworkTelemetryRecorder recorder; + + public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { + this.recorder = recorder; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + + try { + Response response = chain.proceed(request); + + if (response.code() >= 400 && recorder != null) { + recorder.recordNetworkEvent(Level.CRITICAL, request.method(), request.url().toString(), String.valueOf(response.code())); + } + + return response; + + } catch (IOException e) { + if (recorder != null) { + recorder.recordErrorEvent(e); + } + + throw e; + } + } +} diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java new file mode 100644 index 00000000..1ab91181 --- /dev/null +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -0,0 +1,148 @@ +package com.rollbar.okhttp; + +import com.rollbar.api.payload.data.Level; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.SocketPolicy; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class RollbarOkHttpInterceptorTest { + + private MockWebServer server; + private NetworkTelemetryRecorder recorder; + private OkHttpClient client; + + @BeforeEach + void setUp() throws IOException { + server = new MockWebServer(); + server.start(); + + recorder = mock(NetworkTelemetryRecorder.class); + + client = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder)) + .build(); + } + + @AfterEach + void tearDown() throws IOException { + server.shutdown(); + } + + @Test + void successfulResponse_doesNotRecordEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(200)); + + Request request = new Request.Builder().url(server.url("/ok")).build(); + Response response = client.newCall(request).execute(); + response.close(); + + assertEquals(200, response.code()); + verifyNoInteractions(recorder); + } + + @Test + void redirectResponse_doesNotRecordEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location", "/other")); + + OkHttpClient noFollowClient = client.newBuilder().followRedirects(false).build(); + Request request = new Request.Builder().url(server.url("/redirect")).build(); + Response response = noFollowClient.newCall(request).execute(); + response.close(); + + assertEquals(301, response.code()); + verifyNoInteractions(recorder); + } + + @Test + void clientErrorResponse_recordsNetworkEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(404)); + + Request request = new Request.Builder().url(server.url("/not-found")).build(); + Response response = client.newCall(request).execute(); + response.close(); + + assertEquals(404, response.code()); + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), contains("/not-found"), eq("404")); + verify(recorder, never()).recordErrorEvent(any()); + } + + @Test + void serverErrorResponse_recordsNetworkEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = client.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), contains("/error"), eq("500")); + verify(recorder, never()).recordErrorEvent(any()); + } + + @Test + void connectionFailure_recordsErrorEvent() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + + Request request = new Request.Builder().url(server.url("/fail")).build(); + + assertThrows(IOException.class, () -> client.newCall(request).execute()); + + verify(recorder).recordErrorEvent(any(IOException.class)); + verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); + } + + @Test + void postRequest_recordsCorrectMethod() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + Request request = new Request.Builder() + .url(server.url("/post")) + .post(okhttp3.RequestBody.create("body", okhttp3.MediaType.parse("text/plain"))) + .build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("POST"), any(), eq("500")); + } + + @Test + void nullRecorder_errorResponse_doesNotThrowNPE() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient nullRecorderClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(null)) + .build(); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = nullRecorderClient.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + } + + @Test + void nullRecorder_connectionFailure_doesNotThrow() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + + OkHttpClient nullRecorderClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(null)) + .build(); + + Request request = new Request.Builder().url(server.url("/fail")).build(); + + assertThrows(IOException.class, () -> nullRecorderClient.newCall(request).execute()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b56b7aee..4b4dd274 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,7 @@ include( ":rollbar-jakarta-web", ":rollbar-log4j2", ":rollbar-logback", + "rollbar-okhttp", ":rollbar-spring-webmvc", ":rollbar-spring6-webmvc", ":rollbar-spring-boot-webmvc", From e141e7e119db05e90cf20f4a547eac10695b170d Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 24 Mar 2026 15:24:23 -0300 Subject: [PATCH 02/21] build(okhttp): update dependencies --- rollbar-okhttp/build.gradle.kts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index d694a634..8f75bb8b 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -10,12 +10,12 @@ repositories { } dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) + 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:4.9.2") + testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2") testImplementation("org.mockito:mockito-core:5.23.0") - implementation("com.squareup.okhttp3:okhttp:4.9.2") + implementation("com.squareup.okhttp3:okhttp:5.3.2") api(project(":rollbar-api")) } From ab26f4246b21520697f80dc92c5892704521cd27 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 24 Mar 2026 15:32:22 -0300 Subject: [PATCH 03/21] chore(okhttp): add readme --- rollbar-okhttp/README.md | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 rollbar-okhttp/README.md diff --git a/rollbar-okhttp/README.md b/rollbar-okhttp/README.md new file mode 100644 index 00000000..f21290a8 --- /dev/null +++ b/rollbar-okhttp/README.md @@ -0,0 +1,75 @@ +# 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-okhttp:") + implementation("com.squareup.okhttp3:okhttp:") +} +``` + +### Gradle (Groovy) + +```groovy +dependencies { + implementation 'com.rollbar:rollbar-okhttp:' + implementation 'com.squareup.okhttp3:okhttp:' +} +``` + +## Usage + +### 1. Implement `NetworkTelemetryRecorder` + +```java +NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() { + @Override + public void recordNetworkEvent(Level level, String method, String url, String statusCode) { + 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` | From ed9af2002287216ccecfa228a43b6593ebb45222 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Tue, 24 Mar 2026 16:15:43 -0300 Subject: [PATCH 04/21] chore(okhttp): fix lint --- .../okhttp/NetworkTelemetryRecorder.java | 5 +- .../okhttp/RollbarOkHttpInterceptor.java | 47 ++++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java index 7360ec1c..5f5d62ae 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java @@ -3,6 +3,7 @@ import com.rollbar.api.payload.data.Level; public interface NetworkTelemetryRecorder { - void recordNetworkEvent(Level level, String method, String url, String statusCode); - void recordErrorEvent(Exception exception); + void recordNetworkEvent(Level level, String method, String url, String statusCode); + + void recordErrorEvent(Exception exception); } diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index 209fd174..453a0391 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -1,39 +1,44 @@ package com.rollbar.okhttp; import com.rollbar.api.payload.data.Level; + +import java.io.IOException; + import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; -import java.io.IOException; - public class RollbarOkHttpInterceptor implements Interceptor { - private final NetworkTelemetryRecorder recorder; + private final NetworkTelemetryRecorder recorder; - public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { - this.recorder = recorder; - } + public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { + this.recorder = recorder; + } - @Override - public Response intercept(Chain chain) throws IOException { - Request request = chain.request(); + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); - try { - Response response = chain.proceed(request); + try { + Response response = chain.proceed(request); - if (response.code() >= 400 && recorder != null) { - recorder.recordNetworkEvent(Level.CRITICAL, request.method(), request.url().toString(), String.valueOf(response.code())); - } + if (response.code() >= 400 && recorder != null) { + recorder.recordNetworkEvent( + Level.CRITICAL, + request.method(), + request.url().toString(), + String.valueOf(response.code())); + } - return response; + return response; - } catch (IOException e) { - if (recorder != null) { - recorder.recordErrorEvent(e); - } + } catch (IOException e) { + if (recorder != null) { + recorder.recordErrorEvent(e); + } - throw e; - } + throw e; } + } } From 4f2e019b3b1351a762e2426f6781cb3a752db95d Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 20 Apr 2026 01:57:29 -0300 Subject: [PATCH 05/21] fix(okhttp): isolate NetworkTelemetryRecorder failures in interceptor --- .../okhttp/RollbarOkHttpInterceptor.java | 29 +++++++++++++++---- .../okhttp/RollbarOkHttpInterceptorTest.java | 29 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index 453a0391..f3421c6e 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -3,6 +3,7 @@ import com.rollbar.api.payload.data.Level; import java.io.IOException; +import java.util.logging.Logger; import okhttp3.Interceptor; import okhttp3.Request; @@ -10,6 +11,8 @@ public class RollbarOkHttpInterceptor implements Interceptor { + private static final Logger LOGGER = Logger.getLogger(RollbarOkHttpInterceptor.class.getName()); + private final NetworkTelemetryRecorder recorder; public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { @@ -24,18 +27,32 @@ public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(request); if (response.code() >= 400 && recorder != null) { - recorder.recordNetworkEvent( - Level.CRITICAL, - request.method(), - request.url().toString(), - String.valueOf(response.code())); + try { + recorder.recordNetworkEvent( + Level.CRITICAL, + request.method(), + request.url().toString(), + String.valueOf(response.code())); + } catch (Exception recorderException) { + LOGGER.log(java.util.logging.Level.WARNING, + "NetworkTelemetryRecorder.recordNetworkEvent threw an exception; " + + "suppressing to preserve the interceptor contract.", + recorderException); + } } return response; } catch (IOException e) { if (recorder != null) { - recorder.recordErrorEvent(e); + try { + recorder.recordErrorEvent(e); + } catch (Exception recorderException) { + LOGGER.log(java.util.logging.Level.WARNING, + "NetworkTelemetryRecorder.recordErrorEvent threw an exception; " + + "suppressing to preserve the original IOException.", + recorderException); + } } throw e; diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index 1ab91181..c3a5ef13 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -145,4 +145,33 @@ void nullRecorder_connectionFailure_doesNotThrow() { assertThrows(IOException.class, () -> nullRecorderClient.newCall(request).execute()); } + + @Test + void recorderThrowsOnErrorResponse_responseStillReturned() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + doThrow(new RuntimeException("recorder boom")) + .when(recorder) + .recordNetworkEvent(any(), any(), any(), any()); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = client.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + } + + @Test + void recorderThrowsOnConnectionFailure_originalIOExceptionPropagates() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + + doThrow(new RuntimeException("recorder boom")) + .when(recorder) + .recordErrorEvent(any()); + + Request request = new Request.Builder().url(server.url("/fail")).build(); + + assertThrows(IOException.class, () -> client.newCall(request).execute()); + verify(recorder).recordErrorEvent(any(IOException.class)); + } } From e7dd96f943e755acbc6c2f80bbdd2d416a72c3bb Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 20 Apr 2026 02:02:31 -0300 Subject: [PATCH 06/21] build(okhttp): remove hardcoded version to inherit from root --- rollbar-okhttp/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index 8f75bb8b..38ca9ac9 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -3,7 +3,6 @@ plugins { } group = "com.rollbar.okhttp" -version = "2.2.0" repositories { mavenCentral() From ed91c57905d0efd405a86a75190483a8a9b78786 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 20 Apr 2026 18:48:27 -0300 Subject: [PATCH 07/21] build(okhttp): remove redundant plugins and repositories blocks --- rollbar-okhttp/build.gradle.kts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index 38ca9ac9..936dec1a 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -1,13 +1,5 @@ -plugins { - id("java") -} - group = "com.rollbar.okhttp" -repositories { - mavenCentral() -} - dependencies { testImplementation(platform("org.junit:junit-bom:5.14.3")) testImplementation("org.junit.jupiter:junit-jupiter") From 28807a1ff61b4b4bc6056997c8df07f5489b9cbd Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 27 Apr 2026 02:17:49 -0300 Subject: [PATCH 08/21] fix(okhttp): strip query params from recorded URLs by default to prevent sensitive data leakage --- rollbar-okhttp/README.md | 17 ++++++++++ .../okhttp/NetworkTelemetryRecorder.java | 4 +++ .../okhttp/RollbarOkHttpInterceptor.java | 14 +++++++- .../okhttp/RollbarOkHttpInterceptorTest.java | 34 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/rollbar-okhttp/README.md b/rollbar-okhttp/README.md index f21290a8..3cb073a1 100644 --- a/rollbar-okhttp/README.md +++ b/rollbar-okhttp/README.md @@ -35,6 +35,7 @@ dependencies { NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() { @Override public void recordNetworkEvent(Level level, String method, String url, String statusCode) { + // url has query parameters stripped by default (see Security section below) rollbar.recordNetworkEventFor(level, method, url, statusCode); } @@ -73,3 +74,19 @@ The interceptor will automatically record telemetry events to Rollbar without in | 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 + +URL query parameters often carry sensitive data such as API keys (`?api_key=...`), OAuth tokens (`?access_token=...`), or PII. To prevent accidental leakage to Rollbar, the interceptor **strips query parameters by default** before passing the URL to `NetworkTelemetryRecorder`. + +For example, a request to `https://api.example.com/charge?token=sk_live_secret` 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. diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java index 5f5d62ae..174a459e 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java @@ -3,6 +3,10 @@ import com.rollbar.api.payload.data.Level; public interface NetworkTelemetryRecorder { + /** + * @param url the request URL with query parameters stripped by default; supply a custom + * sanitizer to {@link RollbarOkHttpInterceptor} to change this behavior. + */ void recordNetworkEvent(Level level, String method, String url, String statusCode); void recordErrorEvent(Exception exception); diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index f3421c6e..f10fd831 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -3,8 +3,11 @@ import com.rollbar.api.payload.data.Level; import java.io.IOException; +import java.util.Objects; +import java.util.function.Function; import java.util.logging.Logger; +import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; @@ -13,10 +16,19 @@ public class RollbarOkHttpInterceptor implements Interceptor { private static final Logger LOGGER = Logger.getLogger(RollbarOkHttpInterceptor.class.getName()); + private static final Function DEFAULT_URL_SANITIZER = + url -> url.newBuilder().query(null).build().toString(); + private final NetworkTelemetryRecorder recorder; + private final Function urlSanitizer; public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { + this(recorder, DEFAULT_URL_SANITIZER); + } + + public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder, Function urlSanitizer) { this.recorder = recorder; + this.urlSanitizer = Objects.requireNonNull(urlSanitizer, "urlSanitizer must not be null"); } @Override @@ -31,7 +43,7 @@ public Response intercept(Chain chain) throws IOException { recorder.recordNetworkEvent( Level.CRITICAL, request.method(), - request.url().toString(), + urlSanitizer.apply(request.url()), String.valueOf(response.code())); } catch (Exception recorderException) { LOGGER.log(java.util.logging.Level.WARNING, diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index c3a5ef13..f2cbf37a 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -14,6 +14,7 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; class RollbarOkHttpInterceptorTest { @@ -174,4 +175,37 @@ void recorderThrowsOnConnectionFailure_originalIOExceptionPropagates() { assertThrows(IOException.class, () -> client.newCall(request).execute()); verify(recorder).recordErrorEvent(any(IOException.class)); } + + @Test + void defaultSanitizer_stripsQueryParamsFromUrl() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + Request request = new Request.Builder() + .url(server.url("/sensitive?token=sk_live_secret&email=user@example.com")) + .build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), + argThat(url -> url.contains("/sensitive") && !url.contains("secret") && !url.contains("email")), + eq("500")); + } + + @Test + void customSanitizer_isAppliedToUrl() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient customClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder, url -> "Updated String")) + .build(); + + Request request = new Request.Builder() + .url(server.url("/path?secret=abc")) + .build(); + Response response = customClient.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("GET"), eq("Updated String"), eq("500")); + } } From 02c7ea0cae51db6f27c3035fe4888907274a336c Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 27 Apr 2026 02:31:39 -0300 Subject: [PATCH 09/21] fix(okhttp): strip query params from recorded URLs by default to prevent sensitive data leakage --- .../okhttp/NetworkTelemetryRecorder.java | 17 +++++++++++++++-- .../okhttp/RollbarOkHttpInterceptorTest.java | 3 ++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java index 174a459e..3cbef29c 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java @@ -2,12 +2,25 @@ import com.rollbar.api.payload.data.Level; +/** + * Records network telemetry events and errors for HTTP requests. + */ public interface NetworkTelemetryRecorder { /** - * @param url the request URL with query parameters stripped by default; supply a custom - * sanitizer to {@link RollbarOkHttpInterceptor} to change this behavior. + * 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 query parameters 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); } diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index f2cbf37a..09d85a54 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -206,6 +206,7 @@ void customSanitizer_isAppliedToUrl() throws IOException { Response response = customClient.newCall(request).execute(); response.close(); - verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("GET"), eq("Updated String"), eq("500")); + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), eq("Updated String"), eq("500")); } } From fd321cbb7ca9b56bc02c24e0d34df768d996b34f Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 27 Apr 2026 02:33:52 -0300 Subject: [PATCH 10/21] fix(okhttp): lint error, decrease line length --- .../java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index f10fd831..e13a8c6b 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -26,7 +26,9 @@ public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { this(recorder, DEFAULT_URL_SANITIZER); } - public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder, Function urlSanitizer) { + public RollbarOkHttpInterceptor( + NetworkTelemetryRecorder recorder, + Function urlSanitizer) { this.recorder = recorder; this.urlSanitizer = Objects.requireNonNull(urlSanitizer, "urlSanitizer must not be null"); } From b4db532db6e02de2b446ac3caa49851f6c269623 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 16:45:52 -0300 Subject: [PATCH 11/21] fix(okhttp): attribute sanitizer exceptions to urlSanitizer in logs --- .../okhttp/RollbarOkHttpInterceptor.java | 12 +++++++++++- .../okhttp/RollbarOkHttpInterceptorTest.java | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index e13a8c6b..2f6c0a5c 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -41,11 +41,21 @@ public Response intercept(Chain chain) throws IOException { Response response = chain.proceed(request); if (response.code() >= 400 && recorder != null) { + String sanitizedUrl; + try { + sanitizedUrl = urlSanitizer.apply(request.url()); + } catch (Exception sanitizerException) { + LOGGER.log(java.util.logging.Level.WARNING, + "urlSanitizer threw an exception; " + + "suppressing to preserve the interceptor contract.", + sanitizerException); + return response; + } try { recorder.recordNetworkEvent( Level.CRITICAL, request.method(), - urlSanitizer.apply(request.url()), + sanitizedUrl, String.valueOf(response.code())); } catch (Exception recorderException) { LOGGER.log(java.util.logging.Level.WARNING, diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index 09d85a54..8472a00d 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -192,6 +192,23 @@ void defaultSanitizer_stripsQueryParamsFromUrl() throws IOException { eq("500")); } + @Test + void customSanitizerThrows_responseStillReturnedAndRecorderNotCalled() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient throwingClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder, + url -> { throw new IllegalStateException("bad url"); })) + .build(); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = throwingClient.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); + } + @Test void customSanitizer_isAppliedToUrl() throws IOException { server.enqueue(new MockResponse().setResponseCode(500)); From ce27e7b3fb6e254679a70568cf67442043d71467 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 16:53:22 -0300 Subject: [PATCH 12/21] fix(okhttp): strip credentials and fragment from URLs in default sanitizer --- .../okhttp/RollbarOkHttpInterceptor.java | 2 +- .../okhttp/RollbarOkHttpInterceptorTest.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index 2f6c0a5c..a8391c48 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -17,7 +17,7 @@ public class RollbarOkHttpInterceptor implements Interceptor { private static final Logger LOGGER = Logger.getLogger(RollbarOkHttpInterceptor.class.getName()); private static final Function DEFAULT_URL_SANITIZER = - url -> url.newBuilder().query(null).build().toString(); + url -> url.newBuilder().username("").password("").query(null).fragment(null).build().toString(); private final NetworkTelemetryRecorder recorder; private final Function urlSanitizer; diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index 8472a00d..27b71e56 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -1,6 +1,7 @@ package com.rollbar.okhttp; import com.rollbar.api.payload.data.Level; +import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; @@ -192,6 +193,32 @@ void defaultSanitizer_stripsQueryParamsFromUrl() throws IOException { eq("500")); } + @Test + void defaultSanitizer_stripsCredentialsAndFragment() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + HttpUrl urlWithCredentials = server.url("/charge") + .newBuilder() + .username("anyUser") + .password("anyPassword") + .addQueryParameter("token", "abc") + .fragment("section") + .build(); + + Request request = new Request.Builder().url(urlWithCredentials).build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), + argThat(url -> url.contains("/charge") + && !url.contains("anyUser") + && !url.contains("anyPassword") + && !url.contains("token") + && !url.contains("section")), + eq("500")); + } + @Test void customSanitizerThrows_responseStillReturnedAndRecorderNotCalled() throws IOException { server.enqueue(new MockResponse().setResponseCode(500)); From 2c505c1f0515e761c1f52f23ed59153e2919326b Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 16:59:48 -0300 Subject: [PATCH 13/21] fix(okhttp): replace JUL logger with SLF4J to match SDK conventions --- rollbar-okhttp/build.gradle.kts | 1 + .../okhttp/RollbarOkHttpInterceptor.java | 23 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index 936dec1a..6718c2c8 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { testImplementation("org.mockito:mockito-core:5.23.0") implementation("com.squareup.okhttp3:okhttp:5.3.2") api(project(":rollbar-api")) + api("org.slf4j:slf4j-api:1.7.25") } tasks.test { diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index a8391c48..af21149a 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -5,16 +5,17 @@ import java.io.IOException; import java.util.Objects; import java.util.function.Function; -import java.util.logging.Logger; import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class RollbarOkHttpInterceptor implements Interceptor { - private static final Logger LOGGER = Logger.getLogger(RollbarOkHttpInterceptor.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(RollbarOkHttpInterceptor.class); private static final Function DEFAULT_URL_SANITIZER = url -> url.newBuilder().username("").password("").query(null).fragment(null).build().toString(); @@ -45,10 +46,8 @@ public Response intercept(Chain chain) throws IOException { try { sanitizedUrl = urlSanitizer.apply(request.url()); } catch (Exception sanitizerException) { - LOGGER.log(java.util.logging.Level.WARNING, - "urlSanitizer threw an exception; " - + "suppressing to preserve the interceptor contract.", - sanitizerException); + LOGGER.warn("urlSanitizer threw an exception; " + + "suppressing to preserve the interceptor contract.", sanitizerException); return response; } try { @@ -58,10 +57,8 @@ public Response intercept(Chain chain) throws IOException { sanitizedUrl, String.valueOf(response.code())); } catch (Exception recorderException) { - LOGGER.log(java.util.logging.Level.WARNING, - "NetworkTelemetryRecorder.recordNetworkEvent threw an exception; " - + "suppressing to preserve the interceptor contract.", - recorderException); + LOGGER.warn("NetworkTelemetryRecorder.recordNetworkEvent threw an exception; " + + "suppressing to preserve the interceptor contract.", recorderException); } } @@ -72,10 +69,8 @@ public Response intercept(Chain chain) throws IOException { try { recorder.recordErrorEvent(e); } catch (Exception recorderException) { - LOGGER.log(java.util.logging.Level.WARNING, - "NetworkTelemetryRecorder.recordErrorEvent threw an exception; " - + "suppressing to preserve the original IOException.", - recorderException); + LOGGER.warn("NetworkTelemetryRecorder.recordErrorEvent threw an exception; " + + "suppressing to preserve the original IOException.", recorderException); } } From 3c4f505ace239d1fc9f79877866bdcb915d50c92 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 17:02:22 -0300 Subject: [PATCH 14/21] build(okhttp): remove redundant mockito-core declaration --- rollbar-okhttp/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index 6718c2c8..1a1a2bac 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -5,7 +5,6 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter") testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("com.squareup.okhttp3:mockwebserver:5.3.2") - testImplementation("org.mockito:mockito-core:5.23.0") implementation("com.squareup.okhttp3:okhttp:5.3.2") api(project(":rollbar-api")) api("org.slf4j:slf4j-api:1.7.25") From 1d7dab4ac159f5bad99f675d835d67e574524c02 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 17:03:39 -0300 Subject: [PATCH 15/21] docs(okhttp): add rollbar-java to installation snippet --- rollbar-okhttp/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rollbar-okhttp/README.md b/rollbar-okhttp/README.md index 3cb073a1..64801cb3 100644 --- a/rollbar-okhttp/README.md +++ b/rollbar-okhttp/README.md @@ -13,6 +13,7 @@ It records: ```kotlin dependencies { + implementation("com.rollbar:rollbar-java:") implementation("com.rollbar:rollbar-okhttp:") implementation("com.squareup.okhttp3:okhttp:") } @@ -22,6 +23,7 @@ dependencies { ```groovy dependencies { + implementation 'com.rollbar:rollbar-java:' implementation 'com.rollbar:rollbar-okhttp:' implementation 'com.squareup.okhttp3:okhttp:' } From f14a9ebf1d3092d17b8343be2377ac853f3e47af Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 17:15:30 -0300 Subject: [PATCH 16/21] fix(okhttp): lint line length --- .../com/rollbar/okhttp/RollbarOkHttpInterceptor.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index af21149a..49c3f2fa 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -18,7 +18,14 @@ public class RollbarOkHttpInterceptor implements Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RollbarOkHttpInterceptor.class); private static final Function DEFAULT_URL_SANITIZER = - url -> url.newBuilder().username("").password("").query(null).fragment(null).build().toString(); + url -> url + .newBuilder() + .username("") + .password("") + .query(null) + .fragment(null) + .build() + .toString(); private final NetworkTelemetryRecorder recorder; private final Function urlSanitizer; From 0d75c634e1e52165613a36ea84964a2f798f3457 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 18:46:48 -0300 Subject: [PATCH 17/21] style(okhttp): make test class public and use 2-space indentation --- .../okhttp/RollbarOkHttpInterceptorTest.java | 432 +++++++++--------- 1 file changed, 216 insertions(+), 216 deletions(-) diff --git a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java index 27b71e56..2ca8f8d6 100644 --- a/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java +++ b/rollbar-okhttp/src/test/java/com/rollbar/okhttp/RollbarOkHttpInterceptorTest.java @@ -18,239 +18,239 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.*; -class RollbarOkHttpInterceptorTest { +public class RollbarOkHttpInterceptorTest { - private MockWebServer server; - private NetworkTelemetryRecorder recorder; - private OkHttpClient client; + private MockWebServer server; + private NetworkTelemetryRecorder recorder; + private OkHttpClient client; - @BeforeEach - void setUp() throws IOException { - server = new MockWebServer(); - server.start(); + @BeforeEach + public void setUp() throws IOException { + server = new MockWebServer(); + server.start(); - recorder = mock(NetworkTelemetryRecorder.class); - - client = new OkHttpClient.Builder() - .addInterceptor(new RollbarOkHttpInterceptor(recorder)) - .build(); - } + recorder = mock(NetworkTelemetryRecorder.class); + + client = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder)) + .build(); + } - @AfterEach - void tearDown() throws IOException { - server.shutdown(); - } + @AfterEach + public void tearDown() throws IOException { + server.shutdown(); + } - @Test - void successfulResponse_doesNotRecordEvent() throws IOException { - server.enqueue(new MockResponse().setResponseCode(200)); + @Test + public void successfulResponse_doesNotRecordEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(200)); - Request request = new Request.Builder().url(server.url("/ok")).build(); - Response response = client.newCall(request).execute(); - response.close(); + Request request = new Request.Builder().url(server.url("/ok")).build(); + Response response = client.newCall(request).execute(); + response.close(); - assertEquals(200, response.code()); - verifyNoInteractions(recorder); - } + assertEquals(200, response.code()); + verifyNoInteractions(recorder); + } - @Test - void redirectResponse_doesNotRecordEvent() throws IOException { - server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location", "/other")); + @Test + public void redirectResponse_doesNotRecordEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location", "/other")); - OkHttpClient noFollowClient = client.newBuilder().followRedirects(false).build(); - Request request = new Request.Builder().url(server.url("/redirect")).build(); - Response response = noFollowClient.newCall(request).execute(); - response.close(); + OkHttpClient noFollowClient = client.newBuilder().followRedirects(false).build(); + Request request = new Request.Builder().url(server.url("/redirect")).build(); + Response response = noFollowClient.newCall(request).execute(); + response.close(); - assertEquals(301, response.code()); - verifyNoInteractions(recorder); - } + assertEquals(301, response.code()); + verifyNoInteractions(recorder); + } - @Test - void clientErrorResponse_recordsNetworkEvent() throws IOException { - server.enqueue(new MockResponse().setResponseCode(404)); + @Test + public void clientErrorResponse_recordsNetworkEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(404)); - Request request = new Request.Builder().url(server.url("/not-found")).build(); - Response response = client.newCall(request).execute(); - response.close(); + Request request = new Request.Builder().url(server.url("/not-found")).build(); + Response response = client.newCall(request).execute(); + response.close(); - assertEquals(404, response.code()); - verify(recorder).recordNetworkEvent( - eq(Level.CRITICAL), eq("GET"), contains("/not-found"), eq("404")); - verify(recorder, never()).recordErrorEvent(any()); - } + assertEquals(404, response.code()); + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), contains("/not-found"), eq("404")); + verify(recorder, never()).recordErrorEvent(any()); + } - @Test - void serverErrorResponse_recordsNetworkEvent() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); + @Test + public void serverErrorResponse_recordsNetworkEvent() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); - Request request = new Request.Builder().url(server.url("/error")).build(); - Response response = client.newCall(request).execute(); - response.close(); + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = client.newCall(request).execute(); + response.close(); - assertEquals(500, response.code()); - verify(recorder).recordNetworkEvent( - eq(Level.CRITICAL), eq("GET"), contains("/error"), eq("500")); - verify(recorder, never()).recordErrorEvent(any()); - } + assertEquals(500, response.code()); + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), contains("/error"), eq("500")); + verify(recorder, never()).recordErrorEvent(any()); + } - @Test - void connectionFailure_recordsErrorEvent() { - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + @Test + public void connectionFailure_recordsErrorEvent() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - Request request = new Request.Builder().url(server.url("/fail")).build(); + Request request = new Request.Builder().url(server.url("/fail")).build(); - assertThrows(IOException.class, () -> client.newCall(request).execute()); - - verify(recorder).recordErrorEvent(any(IOException.class)); - verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); - } - - @Test - void postRequest_recordsCorrectMethod() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - Request request = new Request.Builder() - .url(server.url("/post")) - .post(okhttp3.RequestBody.create("body", okhttp3.MediaType.parse("text/plain"))) - .build(); - Response response = client.newCall(request).execute(); - response.close(); - - verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("POST"), any(), eq("500")); - } - - @Test - void nullRecorder_errorResponse_doesNotThrowNPE() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - OkHttpClient nullRecorderClient = new OkHttpClient.Builder() - .addInterceptor(new RollbarOkHttpInterceptor(null)) - .build(); - - Request request = new Request.Builder().url(server.url("/error")).build(); - Response response = nullRecorderClient.newCall(request).execute(); - response.close(); - - assertEquals(500, response.code()); - } - - @Test - void nullRecorder_connectionFailure_doesNotThrow() { - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - - OkHttpClient nullRecorderClient = new OkHttpClient.Builder() - .addInterceptor(new RollbarOkHttpInterceptor(null)) - .build(); - - Request request = new Request.Builder().url(server.url("/fail")).build(); - - assertThrows(IOException.class, () -> nullRecorderClient.newCall(request).execute()); - } - - @Test - void recorderThrowsOnErrorResponse_responseStillReturned() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - doThrow(new RuntimeException("recorder boom")) - .when(recorder) - .recordNetworkEvent(any(), any(), any(), any()); - - Request request = new Request.Builder().url(server.url("/error")).build(); - Response response = client.newCall(request).execute(); - response.close(); - - assertEquals(500, response.code()); - } - - @Test - void recorderThrowsOnConnectionFailure_originalIOExceptionPropagates() { - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); - - doThrow(new RuntimeException("recorder boom")) - .when(recorder) - .recordErrorEvent(any()); - - Request request = new Request.Builder().url(server.url("/fail")).build(); - - assertThrows(IOException.class, () -> client.newCall(request).execute()); - verify(recorder).recordErrorEvent(any(IOException.class)); - } - - @Test - void defaultSanitizer_stripsQueryParamsFromUrl() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - Request request = new Request.Builder() - .url(server.url("/sensitive?token=sk_live_secret&email=user@example.com")) - .build(); - Response response = client.newCall(request).execute(); - response.close(); - - verify(recorder).recordNetworkEvent( - eq(Level.CRITICAL), eq("GET"), - argThat(url -> url.contains("/sensitive") && !url.contains("secret") && !url.contains("email")), - eq("500")); - } - - @Test - void defaultSanitizer_stripsCredentialsAndFragment() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - HttpUrl urlWithCredentials = server.url("/charge") - .newBuilder() - .username("anyUser") - .password("anyPassword") - .addQueryParameter("token", "abc") - .fragment("section") - .build(); - - Request request = new Request.Builder().url(urlWithCredentials).build(); - Response response = client.newCall(request).execute(); - response.close(); - - verify(recorder).recordNetworkEvent( - eq(Level.CRITICAL), eq("GET"), - argThat(url -> url.contains("/charge") - && !url.contains("anyUser") - && !url.contains("anyPassword") - && !url.contains("token") - && !url.contains("section")), - eq("500")); - } - - @Test - void customSanitizerThrows_responseStillReturnedAndRecorderNotCalled() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - OkHttpClient throwingClient = new OkHttpClient.Builder() - .addInterceptor(new RollbarOkHttpInterceptor(recorder, - url -> { throw new IllegalStateException("bad url"); })) - .build(); - - Request request = new Request.Builder().url(server.url("/error")).build(); - Response response = throwingClient.newCall(request).execute(); - response.close(); - - assertEquals(500, response.code()); - verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); - } - - @Test - void customSanitizer_isAppliedToUrl() throws IOException { - server.enqueue(new MockResponse().setResponseCode(500)); - - OkHttpClient customClient = new OkHttpClient.Builder() - .addInterceptor(new RollbarOkHttpInterceptor(recorder, url -> "Updated String")) - .build(); - - Request request = new Request.Builder() - .url(server.url("/path?secret=abc")) - .build(); - Response response = customClient.newCall(request).execute(); - response.close(); - - verify(recorder).recordNetworkEvent( - eq(Level.CRITICAL), eq("GET"), eq("Updated String"), eq("500")); - } + assertThrows(IOException.class, () -> client.newCall(request).execute()); + + verify(recorder).recordErrorEvent(any(IOException.class)); + verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); + } + + @Test + public void postRequest_recordsCorrectMethod() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + Request request = new Request.Builder() + .url(server.url("/post")) + .post(okhttp3.RequestBody.create("body", okhttp3.MediaType.parse("text/plain"))) + .build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent(eq(Level.CRITICAL), eq("POST"), any(), eq("500")); + } + + @Test + public void nullRecorder_errorResponse_doesNotThrowNPE() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient nullRecorderClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(null)) + .build(); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = nullRecorderClient.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + } + + @Test + public void nullRecorder_connectionFailure_doesNotThrow() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + + OkHttpClient nullRecorderClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(null)) + .build(); + + Request request = new Request.Builder().url(server.url("/fail")).build(); + + assertThrows(IOException.class, () -> nullRecorderClient.newCall(request).execute()); + } + + @Test + public void recorderThrowsOnErrorResponse_responseStillReturned() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + doThrow(new RuntimeException("recorder boom")) + .when(recorder) + .recordNetworkEvent(any(), any(), any(), any()); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = client.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + } + + @Test + public void recorderThrowsOnConnectionFailure_originalIOExceptionPropagates() { + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)); + + doThrow(new RuntimeException("recorder boom")) + .when(recorder) + .recordErrorEvent(any()); + + Request request = new Request.Builder().url(server.url("/fail")).build(); + + assertThrows(IOException.class, () -> client.newCall(request).execute()); + verify(recorder).recordErrorEvent(any(IOException.class)); + } + + @Test + public void defaultSanitizer_stripsQueryParamsFromUrl() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + Request request = new Request.Builder() + .url(server.url("/sensitive?token=sk_live_secret&email=user@example.com")) + .build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), + argThat(url -> url.contains("/sensitive") && !url.contains("secret") && !url.contains("email")), + eq("500")); + } + + @Test + public void defaultSanitizer_stripsCredentialsAndFragment() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + HttpUrl urlWithCredentials = server.url("/charge") + .newBuilder() + .username("anyUser") + .password("anyPassword") + .addQueryParameter("token", "abc") + .fragment("section") + .build(); + + Request request = new Request.Builder().url(urlWithCredentials).build(); + Response response = client.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), + argThat(url -> url.contains("/charge") + && !url.contains("anyUser") + && !url.contains("anyPassword") + && !url.contains("token") + && !url.contains("section")), + eq("500")); + } + + @Test + public void customSanitizerThrows_responseStillReturnedAndRecorderNotCalled() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient throwingClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder, + url -> { throw new IllegalStateException("bad url"); })) + .build(); + + Request request = new Request.Builder().url(server.url("/error")).build(); + Response response = throwingClient.newCall(request).execute(); + response.close(); + + assertEquals(500, response.code()); + verify(recorder, never()).recordNetworkEvent(any(), any(), any(), any()); + } + + @Test + public void customSanitizer_isAppliedToUrl() throws IOException { + server.enqueue(new MockResponse().setResponseCode(500)); + + OkHttpClient customClient = new OkHttpClient.Builder() + .addInterceptor(new RollbarOkHttpInterceptor(recorder, url -> "Updated String")) + .build(); + + Request request = new Request.Builder() + .url(server.url("/path?secret=abc")) + .build(); + Response response = customClient.newCall(request).execute(); + response.close(); + + verify(recorder).recordNetworkEvent( + eq(Level.CRITICAL), eq("GET"), eq("Updated String"), eq("500")); + } } From 5835a8276a2c8eff9f167fee03e4013248f7efe4 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 18:48:32 -0300 Subject: [PATCH 18/21] style: add missing colon prefix to rollbar-okhttp in settings.gradle.kts --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 4b4dd274..73e10c5b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,7 +38,7 @@ include( ":rollbar-jakarta-web", ":rollbar-log4j2", ":rollbar-logback", - "rollbar-okhttp", + ":rollbar-okhttp", ":rollbar-spring-webmvc", ":rollbar-spring6-webmvc", ":rollbar-spring-boot-webmvc", From 8af01d023921da67e29900644f27609695c89bab Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Fri, 1 May 2026 18:49:58 -0300 Subject: [PATCH 19/21] docs(okhttp): update sanitizer docs to list all stripped URL components --- rollbar-okhttp/README.md | 7 ++++--- .../java/com/rollbar/okhttp/NetworkTelemetryRecorder.java | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rollbar-okhttp/README.md b/rollbar-okhttp/README.md index 64801cb3..14341ad2 100644 --- a/rollbar-okhttp/README.md +++ b/rollbar-okhttp/README.md @@ -37,7 +37,8 @@ dependencies { NetworkTelemetryRecorder recorder = new NetworkTelemetryRecorder() { @Override public void recordNetworkEvent(Level level, String method, String url, String statusCode) { - // url has query parameters stripped by default (see Security section below) + // url has userinfo, query parameters, and fragment stripped by default + // (see Security section below) rollbar.recordNetworkEventFor(level, method, url, statusCode); } @@ -79,9 +80,9 @@ The interceptor will automatically record telemetry events to Rollbar without in ## Security -URL query parameters often carry sensitive data such as API keys (`?api_key=...`), OAuth tokens (`?access_token=...`), or PII. To prevent accidental leakage to Rollbar, the interceptor **strips query parameters by default** before passing the URL to `NetworkTelemetryRecorder`. +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://api.example.com/charge?token=sk_live_secret` will be recorded as `https://api.example.com/charge`. +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: diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java index 3cbef29c..3b00db0e 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/NetworkTelemetryRecorder.java @@ -11,8 +11,9 @@ public interface NetworkTelemetryRecorder { * * @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 query parameters stripped by default; supply a custom - * sanitizer to {@link RollbarOkHttpInterceptor} to change this behavior + * @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); From be12a54fcbb5b5a4837de8eeef85457f5381cd30 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Sat, 2 May 2026 01:07:20 -0300 Subject: [PATCH 20/21] fix(okhttp): replace java.util.function.Function with custom UrlSanitizer interface for API 21 compatibility --- .../rollbar/okhttp/RollbarOkHttpInterceptor.java | 13 +++++++------ .../main/java/com/rollbar/okhttp/UrlSanitizer.java | 8 ++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 rollbar-okhttp/src/main/java/com/rollbar/okhttp/UrlSanitizer.java diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java index 49c3f2fa..edbd709e 100644 --- a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/RollbarOkHttpInterceptor.java @@ -4,12 +4,12 @@ import java.io.IOException; import java.util.Objects; -import java.util.function.Function; -import okhttp3.HttpUrl; import okhttp3.Interceptor; import okhttp3.Request; import okhttp3.Response; + +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,7 +17,7 @@ public class RollbarOkHttpInterceptor implements Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RollbarOkHttpInterceptor.class); - private static final Function DEFAULT_URL_SANITIZER = + private static final UrlSanitizer DEFAULT_URL_SANITIZER = url -> url .newBuilder() .username("") @@ -28,7 +28,7 @@ public class RollbarOkHttpInterceptor implements Interceptor { .toString(); private final NetworkTelemetryRecorder recorder; - private final Function urlSanitizer; + private final UrlSanitizer urlSanitizer; public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { this(recorder, DEFAULT_URL_SANITIZER); @@ -36,11 +36,12 @@ public RollbarOkHttpInterceptor(NetworkTelemetryRecorder recorder) { public RollbarOkHttpInterceptor( NetworkTelemetryRecorder recorder, - Function urlSanitizer) { + 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(); @@ -51,7 +52,7 @@ public Response intercept(Chain chain) throws IOException { if (response.code() >= 400 && recorder != null) { String sanitizedUrl; try { - sanitizedUrl = urlSanitizer.apply(request.url()); + sanitizedUrl = urlSanitizer.sanitize(request.url()); } catch (Exception sanitizerException) { LOGGER.warn("urlSanitizer threw an exception; " + "suppressing to preserve the interceptor contract.", sanitizerException); diff --git a/rollbar-okhttp/src/main/java/com/rollbar/okhttp/UrlSanitizer.java b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/UrlSanitizer.java new file mode 100644 index 00000000..6dfa6106 --- /dev/null +++ b/rollbar-okhttp/src/main/java/com/rollbar/okhttp/UrlSanitizer.java @@ -0,0 +1,8 @@ +package com.rollbar.okhttp; + +import okhttp3.HttpUrl; + +@FunctionalInterface +public interface UrlSanitizer { + String sanitize(HttpUrl url); +} From 106f26db8e44a5e61b83fafe040fab86af141071 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 4 May 2026 20:30:54 -0300 Subject: [PATCH 21/21] fix(okhttp): remove incorrect group override so module publishes as com.rollbar:rollbar-okhttp --- rollbar-okhttp/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/rollbar-okhttp/build.gradle.kts b/rollbar-okhttp/build.gradle.kts index 1a1a2bac..ff3b8bf3 100644 --- a/rollbar-okhttp/build.gradle.kts +++ b/rollbar-okhttp/build.gradle.kts @@ -1,5 +1,3 @@ -group = "com.rollbar.okhttp" - dependencies { testImplementation(platform("org.junit:junit-bom:5.14.3")) testImplementation("org.junit.jupiter:junit-jupiter")