mirror of
https://github.com/openflocon/Flocon.git
synced 2026-06-24 00:49:11 +00:00
fix: [COMPRESSION] brotli (#457)
Some checks are pending
docs / deploy (push) Waiting to run
Some checks are pending
docs / deploy (push) Waiting to run
Co-authored-by: Florent Champigny <florent@bere.al>
This commit is contained in:
parent
d4163217d5
commit
e431bc75b3
11 changed files with 145 additions and 6 deletions
3
FloconAndroid/.vscode/settings.json
vendored
Normal file
3
FloconAndroid/.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ processPhoenix = "3.0.0"
|
|||
sqlite = "2.5.2"
|
||||
sqliteJdbc = "3.50.3.0"
|
||||
buildconfig = "5.6.8"
|
||||
brotli = "0.1.2"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
|
|
@ -38,6 +39,7 @@ androidx-datastore-preferences = { module = "androidx.datastore:datastore-prefer
|
|||
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
|
||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||
brotli-dec = { module = "org.brotli:dec", version.ref = "brotli" }
|
||||
apollo-http-okhttprealization = { module = "com.apollographql.apollo:apollo-http-okhttprealization", version.ref = "apollo" }
|
||||
apollo-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
|
||||
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" }
|
||||
|
|
|
|||
|
|
@ -30,11 +30,13 @@ kotlin {
|
|||
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.brotli.dec)
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.brotli.dec)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package io.github.openflocon.flocon.ktor
|
||||
|
||||
import org.brotli.dec.BrotliInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
internal actual fun decodeNetworkBody(bytes: ByteArray, headers: Map<String, String>): String {
|
||||
return if (headers.isBrotli()) {
|
||||
BrotliInputStream(bytes.inputStream()).use { it.readBytes().toString(Charsets.UTF_8) }
|
||||
} else {
|
||||
bytes.decodeToString()
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,13 @@ val FloconKtorPlugin = createClientPlugin("FloconKtorPlugin", ::FloconKtorPlugin
|
|||
val floconCallResponse = FloconNetworkResponse(
|
||||
httpCode = response.status.value,
|
||||
contentType = contentType,
|
||||
body = if(isImage) null else originalBodyBytes.decodeToString(),
|
||||
body = if (isImage) null else {
|
||||
if (responseHeadersMap.isBrotli()) {
|
||||
decodeNetworkBody(originalBodyBytes, responseHeadersMap)
|
||||
} else {
|
||||
originalBodyBytes.decodeToString()
|
||||
}
|
||||
},
|
||||
headers = responseHeadersMap,
|
||||
size = responseSize,
|
||||
grpcStatus = null,
|
||||
|
|
|
|||
|
|
@ -56,4 +56,21 @@ internal suspend fun extractAndReplaceRequestBody(request: HttpRequestBuilder):
|
|||
is OutgoingContent.NoContent -> null
|
||||
else -> originalBody?.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Map<String, String>.getContentEncoding() : String? {
|
||||
return get("Content-Encoding") ?: get("content-encoding")
|
||||
}
|
||||
|
||||
private fun Map<String, String>.isGzipped(): Boolean {
|
||||
return "gzip".equals(getContentEncoding(), ignoreCase = true)
|
||||
}
|
||||
|
||||
internal fun Map<String, String>.isBrotli(): Boolean {
|
||||
return "br".equals(getContentEncoding(), ignoreCase = true)
|
||||
}
|
||||
|
||||
internal expect fun decodeNetworkBody(
|
||||
bytes: ByteArray,
|
||||
headers: Map<String, String>
|
||||
): String
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package io.github.openflocon.flocon.ktor
|
||||
|
||||
internal actual fun decodeNetworkBody(bytes: ByteArray, headers: Map<String, String>): String {
|
||||
// iOS does not have Brotli library; fallback to UTF-8 string
|
||||
return bytes.decodeToString()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package io.github.openflocon.flocon.ktor
|
||||
|
||||
import org.brotli.dec.BrotliInputStream
|
||||
|
||||
internal actual fun decodeNetworkBody(bytes: ByteArray, headers: Map<String, String>): String {
|
||||
return if (headers.isBrotli()) {
|
||||
BrotliInputStream(bytes.inputStream()).use { it.readBytes().toString(Charsets.UTF_8) }
|
||||
} else {
|
||||
bytes.decodeToString()
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,9 @@ dependencies {
|
|||
|
||||
implementation(platform(libs.okhttp.bom))
|
||||
implementation(libs.okhttp3.okhttp)
|
||||
implementation(libs.brotli.dec)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import okhttp3.Request
|
|||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import okio.GzipSource
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import org.brotli.dec.BrotliInputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
internal fun getHttpMessage(httpCode: Int): String {
|
||||
|
|
@ -73,6 +76,7 @@ internal fun extractResponseBodyInfo(
|
|||
}
|
||||
|
||||
val charset = responseBody.contentType().charsetOrUtf8()
|
||||
bodySize = buffer.size
|
||||
if (responseHeaders.isGzipped()) {
|
||||
GzipSource(buffer.clone()).use { gzippedResponseBody ->
|
||||
buffer = Buffer()
|
||||
|
|
@ -80,10 +84,15 @@ internal fun extractResponseBodyInfo(
|
|||
}
|
||||
|
||||
bodyString = buffer.clone().readString(charset)
|
||||
bodySize = buffer.size
|
||||
} else if (responseHeaders.isBrotli()) {
|
||||
BrotliInputStream(buffer.clone().inputStream()).source().buffer().use { brotliResponseBody ->
|
||||
buffer = Buffer()
|
||||
buffer.writeAll(brotliResponseBody)
|
||||
}
|
||||
|
||||
bodyString = buffer.clone().readString(charset)
|
||||
} else {
|
||||
bodyString = buffer.clone().readString(charset)
|
||||
bodySize = buffer.size
|
||||
}
|
||||
|
||||
return bodyString to bodySize
|
||||
|
|
@ -100,12 +109,17 @@ internal fun extractRequestBodyInfo(
|
|||
var buffer = Buffer()
|
||||
requestBody.writeTo(buffer)
|
||||
|
||||
bodySize = buffer.size
|
||||
if (requestHeaders.isGzipped()) {
|
||||
bodySize = buffer.size
|
||||
GzipSource(buffer).use { gzippedResponseBody ->
|
||||
buffer = Buffer()
|
||||
buffer.writeAll(gzippedResponseBody)
|
||||
}
|
||||
} else if (requestHeaders.isBrotli()) {
|
||||
BrotliInputStream(buffer.inputStream()).source().buffer().use { brotliResponseBody ->
|
||||
buffer = Buffer()
|
||||
buffer.writeAll(brotliResponseBody)
|
||||
}
|
||||
}
|
||||
|
||||
val charset = requestBody.contentType().charsetOrUtf8()
|
||||
|
|
@ -114,7 +128,16 @@ internal fun extractRequestBodyInfo(
|
|||
return bodyString to bodySize
|
||||
}
|
||||
|
||||
private fun Map<String, String>.getContentEncoding() : String? {
|
||||
return get("Content-Encoding") ?: get("content-encoding")
|
||||
}
|
||||
|
||||
private fun Map<String, String>.isGzipped(): Boolean {
|
||||
val contentEncoding = get("Content-Encoding") ?: get("content-encoding") ?: return false
|
||||
val contentEncoding = getContentEncoding() ?: return false
|
||||
return "gzip".equals(contentEncoding, ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun Map<String, String>.isBrotli(): Boolean {
|
||||
val contentEncoding = getContentEncoding() ?: return false
|
||||
return "br".equals(contentEncoding, ignoreCase = true)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package io.github.openflocon.flocon.okhttp
|
||||
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okio.Buffer
|
||||
import okio.ByteString.Companion.decodeHex
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class UtilsTest {
|
||||
|
||||
@Test
|
||||
fun `extractResponseBodyInfo decodes brotli stream`() {
|
||||
// "Hello, Brotli!" compressed with Brotli
|
||||
val brotliHex = "8b068048656c6c6f2c2042726f746c692103"
|
||||
val brotliBytes = brotliHex.decodeHex().toByteArray()
|
||||
|
||||
val responseBody = brotliBytes.toResponseBody("application/json".toMediaType())
|
||||
|
||||
val response = Response.Builder()
|
||||
.request(Request.Builder().url("https://example.com").build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
.message("OK")
|
||||
.header("Content-Encoding", "br")
|
||||
.body(responseBody)
|
||||
.build()
|
||||
|
||||
val (bodyString, bodySize) = extractResponseBodyInfo(response, response.headers.toMap())
|
||||
|
||||
assertEquals("Hello, Brotli!", bodyString)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extractResponseBodyInfo handles plain text`() {
|
||||
val plainText = "Hello, World!"
|
||||
val responseBody = plainText.toResponseBody("text/plain".toMediaType())
|
||||
|
||||
val response = Response.Builder()
|
||||
.request(Request.Builder().url("https://example.com").build())
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
.message("OK")
|
||||
.body(responseBody)
|
||||
.build()
|
||||
|
||||
val (bodyString, _) = extractResponseBodyInfo(response, response.headers.toMap())
|
||||
|
||||
assertEquals("Hello, World!", bodyString)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue