From 51be585b1e334489eb6b34f819768704de69b558 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 14 Oct 2025 13:42:52 +0200 Subject: [PATCH] fix: use stream for response body Signed-off-by: alperozturk --- .../com/nextcloud/utils/BodyHelperTests.kt | 62 +++++++++++++++++++ .../com/nextcloud/common/OkHttpMethodBase.kt | 7 ++- .../android/lib/common/utils/BodyHelper.kt | 47 ++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 library/src/androidTest/java/com/nextcloud/utils/BodyHelperTests.kt create mode 100644 library/src/main/java/com/owncloud/android/lib/common/utils/BodyHelper.kt diff --git a/library/src/androidTest/java/com/nextcloud/utils/BodyHelperTests.kt b/library/src/androidTest/java/com/nextcloud/utils/BodyHelperTests.kt new file mode 100644 index 0000000000..dd77736bf9 --- /dev/null +++ b/library/src/androidTest/java/com/nextcloud/utils/BodyHelperTests.kt @@ -0,0 +1,62 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.nextcloud.utils + +import com.owncloud.android.lib.common.utils.BodyHelper +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Test + +class BodyHelperTests { + companion object { + private const val MAX_RESPONSE_BODY_SIZE = 1_048_576 + private const val ONE_HUNDRED = 100 + } + + @Test + fun testParseResponseWhenBodyIsSmallerThanLimitShouldReturnFullContent() { + val content = "Hello World" + val body = content.toResponseBody("text/plain".toMediaTypeOrNull()) + + val result = BodyHelper.parseResponse(body) + + assertEquals(content, result) + } + + @Test + fun testParseResponseWhenBodyIsExactlyAtLimitShouldReturnFullContent() { + val content = "a".repeat(MAX_RESPONSE_BODY_SIZE) + val body = content.toResponseBody("text/plain".toMediaTypeOrNull()) + + val result = BodyHelper.parseResponse(body) + + assertEquals(content, result) + } + + @Test + fun testParseResponseWhenBodyIsLargerThanLimitShouldReturnTruncatedContentWithNotice() { + val content = "b".repeat(MAX_RESPONSE_BODY_SIZE + ONE_HUNDRED) + val body = content.toResponseBody("text/plain".toMediaTypeOrNull()) + + val result = BodyHelper.parseResponse(body) + + val expectedPrefix = "b".repeat(MAX_RESPONSE_BODY_SIZE) + val expectedSuffix = "\n...[truncated output; showing first 1024 KB]..." + + assert(result.startsWith(expectedPrefix.take(ONE_HUNDRED))) + assert(result.endsWith(expectedSuffix)) + } + + @Test + fun testParseResponseWhenBodyIsEmptyShouldReturnEmptyString() { + val body = "".toResponseBody("text/plain".toMediaTypeOrNull()) + val result = BodyHelper.parseResponse(body) + assertEquals("", result) + } +} diff --git a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt index aba9c23917..e269313225 100644 --- a/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt +++ b/library/src/main/java/com/nextcloud/common/OkHttpMethodBase.kt @@ -9,8 +9,10 @@ */ package com.nextcloud.common +import com.nextcloud.common.OkHttpMethodBase.Companion.UNKNOWN_STATUS_CODE import com.owncloud.android.lib.common.OwnCloudClientManagerFactory import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.utils.BodyHelper import com.owncloud.android.lib.common.utils.Log_OC import okhttp3.Headers import okhttp3.HttpUrl @@ -82,7 +84,10 @@ abstract class OkHttpMethodBase( queryMap = params } - fun getResponseBodyAsString(): String = response?.body?.string() ?: "" + fun getResponseBodyAsString(): String { + val body = response?.body ?: return "" + return BodyHelper.parseResponse(body) + } fun getResponseContentLength(): Long = response?.body?.contentLength() ?: -1 diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/BodyHelper.kt b/library/src/main/java/com/owncloud/android/lib/common/utils/BodyHelper.kt new file mode 100644 index 0000000000..11142674f8 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/BodyHelper.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.common.utils + +import okhttp3.ResponseBody + +object BodyHelper { + private const val MAX_RESPONSE_BODY_SIZE: Long = 1_048_576 + private const val MB = 1024 + private const val BUFFER = 8192 + + @Suppress("NestedBlockDepth") + fun parseResponse(body: ResponseBody): String { + body.byteStream().use { input -> + val buffer = ByteArray(BUFFER) + val output = StringBuilder() + var bytesRead: Int + var totalRead = 0L + var wasTruncated = false + + while (input.read(buffer).also { bytesRead = it } != -1) { + if (totalRead + bytesRead > MAX_RESPONSE_BODY_SIZE) { + val remaining = (MAX_RESPONSE_BODY_SIZE - totalRead).toInt() + if (remaining > 0) { + output.append(String(buffer, 0, remaining, Charsets.UTF_8)) + } + wasTruncated = true + break + } else { + output.append(String(buffer, 0, bytesRead, Charsets.UTF_8)) + totalRead += bytesRead + } + } + + if (wasTruncated) { + output.append("\n...[truncated output; showing first ${MAX_RESPONSE_BODY_SIZE / MB} KB]...") + } + + return output.toString() + } + } +}