Skip to content

bug(android): interceptor buffers full response body in memory — OOM risk for large responses #17

Description

@imsankalp

Summary

NetworkToolsInterceptor.captureResponseBody() reads the entire response body into memory by calling source.request(Long.MAX_VALUE). For large responses (images, file downloads, big JSON payloads) this duplicates the entire response in RAM, risking OutOfMemoryError or significant GC pressure.

Affected code

android/src/main/java/com/networktools/interceptor/NetworkToolsInterceptor.kt:

private fun captureResponseBody(response: Response): String? {
    return try {
        val source = response.body?.source()
        source?.request(Long.MAX_VALUE)     // reads entire body into buffer
        val buffer = source?.buffer
        buffer?.clone()?.readUtf8()          // clones the full buffer again
    } catch (e: Exception) {
        null
    }
}

For a 50 MB image download this allocates ~100 MB of heap (once in the source buffer, once in the clone). The same issue exists in captureRequestBody() for large upload payloads.

Fix — enforce a maxBodySize cap

private val MAX_CAPTURE_BYTES = 256 * 1024L  // 256 KB default, configurable

private fun captureResponseBody(response: Response): String? {
    return try {
        val contentType = response.body?.contentType()?.toString() ?: ""
        if (isBinaryContentType(contentType)) return "[binary content — not captured]"

        val source = response.body?.source()
        source?.request(MAX_CAPTURE_BYTES)
        val snapshot = source?.buffer?.snapshot() ?: return null

        val capturedBytes = minOf(snapshot.size, MAX_CAPTURE_BYTES)
        val body = snapshot.substring(0, capturedBytes).utf8()
        if (snapshot.size > MAX_CAPTURE_BYTES) "$body\n[... truncated — ${snapshot.size} bytes total]"
        else body
    } catch (e: Exception) {
        null
    }
}

private fun isBinaryContentType(contentType: String): Boolean {
    val binaryPrefixes = listOf("image/", "video/", "audio/", "application/octet-stream", "application/zip")
    return binaryPrefixes.any { contentType.startsWith(it) }
}

Expose the limit as a configurable property on NetworkToolsManager:

object NetworkToolsManager {
    var maxBodyCaptureBytes: Long = 256 * 1024L
}

Acceptance criteria

  • captureResponseBody reads at most maxBodyCaptureBytes (default 256 KB)
  • Truncated bodies include a [... truncated — N bytes total] marker
  • Binary content types return [binary content — not captured] placeholder
  • captureRequestBody applies the same cap
  • NetworkToolsManager.maxBodyCaptureBytes is publicly settable
  • NetworkMonitorProvider accepts a maxBodyCaptureBytes prop and passes it through to native
  • Unit test: 1 MB response → captured string ≤ 256 KB + truncation marker
  • Unit test: image/png response → returns binary placeholder, no body bytes stored

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions