From 95e874b9183fe2e3f737556962b5b7f23ff61519 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 8 Jan 2026 16:39:25 +0000 Subject: [PATCH 1/3] fix: Stop writing params in upload before config is complete References: https://outsystemsrd.atlassian.net/browse/RMET-4892 --- .../ionfiletransferlib/IONFLTRController.kt | 21 +++---- .../helpers/IONFLTRConnectionHelper.kt | 56 ++++++++++++------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt index 961ad72..6bf82dc 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt @@ -2,6 +2,7 @@ package io.ionic.libs.ionfiletransferlib import android.content.Context import io.ionic.libs.ionfiletransferlib.helpers.FileToUploadInfo +import io.ionic.libs.ionfiletransferlib.helpers.HttpConnectionSetup import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRConnectionHelper import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRFileHelper import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRInputsValidator @@ -56,9 +57,9 @@ class IONFLTRController internal constructor( fun downloadFile(options: IONFLTRDownloadOptions): Flow = flow { runCatchingIONFLTRExceptions { // Prepare for download - val (targetFile, connection) = prepareForDownload(options) + val (targetFile, connectionSetup) = prepareForDownload(options) - connection.use { conn -> + connectionSetup.connection.use { conn -> // Execute the download and handle response val contentLength = beginDownload(conn) @@ -94,9 +95,9 @@ class IONFLTRController internal constructor( fun uploadFile(options: IONFLTRUploadOptions): Flow = flow { runCatchingIONFLTRExceptions { // Prepare for upload - val (file, connection) = prepareForUpload(options) + val (file, connectionSetup) = prepareForUpload(options) - connection.use { conn -> + connectionSetup.connection.use { conn -> // Execute the upload and handle response val multiPartFormData = beginUpload(conn, options, file) @@ -116,7 +117,7 @@ class IONFLTRController internal constructor( /** * Prepares for download by validating inputs, creating directories and setting up connection. */ - private fun prepareForDownload(options: IONFLTRDownloadOptions): Pair { + private fun prepareForDownload(options: IONFLTRDownloadOptions): Pair { // Validate inputs val normalizedFilePath = fileHelper.normalizeFilePath(options.filePath) inputsValidator.validateTransferInputs(options.url, normalizedFilePath) @@ -126,9 +127,9 @@ class IONFLTRController internal constructor( fileHelper.createParentDirectories(targetFile) // Setup connection - val connection = connectionHelper.setupConnection(options.url, options.httpOptions) + val connectionSetup = connectionHelper.setupConnection(options.url, options.httpOptions) - return Pair(targetFile, connection) + return Pair(targetFile, connectionSetup) } /** @@ -242,7 +243,7 @@ class IONFLTRController internal constructor( /** * Prepares for upload by validating inputs and setting up connection. */ - private fun prepareForUpload(options: IONFLTRUploadOptions): Pair { + private fun prepareForUpload(options: IONFLTRUploadOptions): Pair { // Validate inputs inputsValidator.validateTransferInputs(options.url, options.filePath) @@ -250,9 +251,9 @@ class IONFLTRController internal constructor( val file = fileHelper.getFileToUploadInfo(options.filePath) // Setup connection - val connection = connectionHelper.setupConnection(options.url, options.httpOptions) + val connectionSetup = connectionHelper.setupConnection(options.url, options.httpOptions) - return Pair(file, connection) + return Pair(file, connectionSetup) } /** diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt index 03ed379..928dea7 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt @@ -46,11 +46,11 @@ fun HttpURLConnection.assertSuccessHttpResponse() { /** * Helper class for setting up HTTP connections with proper configuration. */ -class IONFLTRConnectionHelper { +internal class IONFLTRConnectionHelper { /** * Sets up the HTTP connection with the provided options. */ - fun setupConnection(urlString: String, httpOptions: IONFLTRTransferHttpOptions): HttpURLConnection { + fun setupConnection(urlString: String, httpOptions: IONFLTRTransferHttpOptions): HttpConnectionSetup { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection @@ -65,44 +65,58 @@ class IONFLTRConnectionHelper { httpOptions.headers.forEach { (key, value) -> connection.setRequestProperty(key, value) } - + + val isHttpGET = httpOptions.method.equals("GET", ignoreCase = true) + val encodeParams = httpOptions.shouldEncodeUrlParams && isHttpGET + + // Set redirect handling + connection.instanceFollowRedirects = !httpOptions.disableRedirects + + // Set SSL factory if provided + if (httpOptions.sslSocketFactory != null && connection is javax.net.ssl.HttpsURLConnection) { + connection.sslSocketFactory = httpOptions.sslSocketFactory + } + // Set parameters + var paramString = "" if (httpOptions.params.isNotEmpty()) { - val paramString = buildString { + paramString = buildString { httpOptions.params.forEach { (key, values) -> values.forEach { value -> if (isNotEmpty()) append("&") - val encodedKey = if (httpOptions.shouldEncodeUrlParams) { + val encodedKey = if (encodeParams) { java.net.URLEncoder.encode(key, StandardCharsets.UTF_8.name()) } else key - val encodedValue = if (httpOptions.shouldEncodeUrlParams) { + val encodedValue = if (encodeParams) { java.net.URLEncoder.encode(value, StandardCharsets.UTF_8.name()) } else value append("$encodedKey=$encodedValue") } } } - - if (httpOptions.method.equals("GET", ignoreCase = true)) { + + // for requests other than GET, params will be written in the request body. + if (isHttpGET) { val separator = if (urlString.contains("?")) "&" else "?" val newUrl = URL("$urlString$separator$paramString") - return newUrl.openConnection() as HttpURLConnection + return HttpConnectionSetup( + connection = newUrl.openConnection() as HttpURLConnection, + paramStringToWrite = "" + ) } else { - connection.doOutput = true + // TODO move this block elsewhere + /*connection.doOutput = true connection.outputStream.use { os -> os.write(paramString.toByteArray()) - } + }*/ } } - // Set redirect handling - connection.instanceFollowRedirects = !httpOptions.disableRedirects - - // Set SSL factory if provided - if (httpOptions.sslSocketFactory != null && connection is javax.net.ssl.HttpsURLConnection) { - connection.sslSocketFactory = httpOptions.sslSocketFactory - } - - return connection + return HttpConnectionSetup(connection = connection, paramStringToWrite = paramString) } -} \ No newline at end of file +} + +internal data class HttpConnectionSetup( + val connection: HttpURLConnection, + val paramStringToWrite: String +) \ No newline at end of file From 7d7a9f4f19dab442009bbe6b87bf20261a824e45 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Thu, 8 Jan 2026 17:47:12 +0000 Subject: [PATCH 2/3] fix: Write params in url except for multipart/form-data References: https://outsystemsrd.atlassian.net/browse/RMET-4892 --- .../ionfiletransferlib/IONFLTRController.kt | 60 ++++++++--------- .../helpers/IONFLTRConnectionHelper.kt | 64 +++++++++---------- 2 files changed, 58 insertions(+), 66 deletions(-) diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt index 6bf82dc..b568c9a 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/IONFLTRController.kt @@ -2,7 +2,6 @@ package io.ionic.libs.ionfiletransferlib import android.content.Context import io.ionic.libs.ionfiletransferlib.helpers.FileToUploadInfo -import io.ionic.libs.ionfiletransferlib.helpers.HttpConnectionSetup import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRConnectionHelper import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRFileHelper import io.ionic.libs.ionfiletransferlib.helpers.IONFLTRInputsValidator @@ -57,9 +56,9 @@ class IONFLTRController internal constructor( fun downloadFile(options: IONFLTRDownloadOptions): Flow = flow { runCatchingIONFLTRExceptions { // Prepare for download - val (targetFile, connectionSetup) = prepareForDownload(options) + val (targetFile, connection) = prepareForDownload(options) - connectionSetup.connection.use { conn -> + connection.use { conn -> // Execute the download and handle response val contentLength = beginDownload(conn) @@ -95,9 +94,9 @@ class IONFLTRController internal constructor( fun uploadFile(options: IONFLTRUploadOptions): Flow = flow { runCatchingIONFLTRExceptions { // Prepare for upload - val (file, connectionSetup) = prepareForUpload(options) + val (file, connection) = prepareForUpload(options) - connectionSetup.connection.use { conn -> + connection.use { conn -> // Execute the upload and handle response val multiPartFormData = beginUpload(conn, options, file) @@ -117,7 +116,7 @@ class IONFLTRController internal constructor( /** * Prepares for download by validating inputs, creating directories and setting up connection. */ - private fun prepareForDownload(options: IONFLTRDownloadOptions): Pair { + private fun prepareForDownload(options: IONFLTRDownloadOptions): Pair { // Validate inputs val normalizedFilePath = fileHelper.normalizeFilePath(options.filePath) inputsValidator.validateTransferInputs(options.url, normalizedFilePath) @@ -127,9 +126,11 @@ class IONFLTRController internal constructor( fileHelper.createParentDirectories(targetFile) // Setup connection - val connectionSetup = connectionHelper.setupConnection(options.url, options.httpOptions) + val connection = connectionHelper.setupConnection(options.url, options.httpOptions).let { + connectionHelper.appendParamsToUrl(options.url, it, options.httpOptions, useChunkedMode = false) + } - return Pair(targetFile, connectionSetup) + return Pair(targetFile, connection) } /** @@ -243,7 +244,7 @@ class IONFLTRController internal constructor( /** * Prepares for upload by validating inputs and setting up connection. */ - private fun prepareForUpload(options: IONFLTRUploadOptions): Pair { + private fun prepareForUpload(options: IONFLTRUploadOptions): Pair { // Validate inputs inputsValidator.validateTransferInputs(options.url, options.filePath) @@ -251,9 +252,11 @@ class IONFLTRController internal constructor( val file = fileHelper.getFileToUploadInfo(options.filePath) // Setup connection - val connectionSetup = connectionHelper.setupConnection(options.url, options.httpOptions) + val connection = connectionHelper.setupConnection(options.url, options.httpOptions).let { + connectionHelper.appendParamsToUrl(options.url, it, options.httpOptions, options.chunkedMode) + } - return Pair(file, connectionSetup) + return Pair(file, connection) } /** @@ -269,18 +272,16 @@ class IONFLTRController internal constructor( ): Pair? { var multiPartUpload = false // Set content type if not already set - if (!options.httpOptions.headers.containsKey("Content-Type")) { + if (connectionHelper.useMultipartFormData(options.httpOptions) && !useChunkedMode) { + multiPartUpload = true + connection.setRequestProperty( + "Content-Type", + "multipart/form-data; boundary=$BOUNDARY" + ) + } else if (!options.httpOptions.headers.containsKey("Content-Type")) { val mimeType = options.mimeType ?: fileHelper.getMimeType(options.filePath) ?: "application/octet-stream" - if (isPostOrPutMethod(options.httpOptions.method)) { - multiPartUpload = true - connection.setRequestProperty( - "Content-Type", - "multipart/form-data; boundary=$BOUNDARY" - ) - } else { - connection.setRequestProperty("Content-Type", mimeType) - } + connection.setRequestProperty("Content-Type", mimeType) } if (useChunkedMode) { @@ -315,8 +316,9 @@ class IONFLTRController internal constructor( val boundary = "$LINE_START$BOUNDARY$LINE_END" val beforeData = buildString { - // Write additional form parameters if any - options.formParams?.forEach { (key, value) -> + // Write form parameters using the available attributes + val allParams = (options.formParams ?: emptyMap()) + options.httpOptions.params + allParams.forEach { (key, value) -> append(boundary) val paramHeader = "Content-Disposition: form-data; name=\"$key\"$LINE_END$LINE_END" val paramValue = "$value$LINE_END" @@ -396,7 +398,7 @@ class IONFLTRController internal constructor( emit(createUploadFileProgress(bytes = 0, total = 0)) return 0L } - + var totalBytesWritten: Long connection.outputStream.use { connOutputStream -> @@ -447,14 +449,4 @@ class IONFLTRController internal constructor( ) ) } - - /** - * Checks if the HTTP method is either POST or PUT. - * - * @param method The HTTP method to check - * @return True if the method is POST or PUT, false otherwise - */ - private fun isPostOrPutMethod(method: String): Boolean { - return method.equals("POST", ignoreCase = true) || method.equals("PUT", ignoreCase = true) - } } \ No newline at end of file diff --git a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt index 928dea7..e5a71fe 100644 --- a/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt +++ b/src/main/kotlin/io/ionic/libs/ionfiletransferlib/helpers/IONFLTRConnectionHelper.kt @@ -26,7 +26,7 @@ inline fun HttpURLConnection.use(block: (HttpURLConnection) -> R): R { * Extension function to assert that an HTTP response was successful (2xx status code). * If the response was not successful, throws an IONFLTRException.HttpError with details * from the error stream. - * + * * @throws IONFLTRException.HttpError if the response code is not in the 200-299 range */ @@ -50,25 +50,22 @@ internal class IONFLTRConnectionHelper { /** * Sets up the HTTP connection with the provided options. */ - fun setupConnection(urlString: String, httpOptions: IONFLTRTransferHttpOptions): HttpConnectionSetup { + fun setupConnection(urlString: String, httpOptions: IONFLTRTransferHttpOptions): HttpURLConnection { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection - + // Set method connection.requestMethod = httpOptions.method - + // Set timeouts connection.connectTimeout = httpOptions.connectTimeout connection.readTimeout = httpOptions.readTimeout - + // Set headers httpOptions.headers.forEach { (key, value) -> connection.setRequestProperty(key, value) } - val isHttpGET = httpOptions.method.equals("GET", ignoreCase = true) - val encodeParams = httpOptions.shouldEncodeUrlParams && isHttpGET - // Set redirect handling connection.instanceFollowRedirects = !httpOptions.disableRedirects @@ -77,17 +74,27 @@ internal class IONFLTRConnectionHelper { connection.sslSocketFactory = httpOptions.sslSocketFactory } - // Set parameters - var paramString = "" + return connection + } + + /** + * append params to url, if applicable + */ + fun appendParamsToUrl( + urlString: String, + connection: HttpURLConnection, + httpOptions: IONFLTRTransferHttpOptions, + useChunkedMode: Boolean + ): HttpURLConnection { if (httpOptions.params.isNotEmpty()) { - paramString = buildString { + val paramString = buildString { httpOptions.params.forEach { (key, values) -> values.forEach { value -> if (isNotEmpty()) append("&") - val encodedKey = if (encodeParams) { + val encodedKey = if (httpOptions.shouldEncodeUrlParams) { java.net.URLEncoder.encode(key, StandardCharsets.UTF_8.name()) } else key - val encodedValue = if (encodeParams) { + val encodedValue = if (httpOptions.shouldEncodeUrlParams) { java.net.URLEncoder.encode(value, StandardCharsets.UTF_8.name()) } else value append("$encodedKey=$encodedValue") @@ -95,28 +102,21 @@ internal class IONFLTRConnectionHelper { } } - // for requests other than GET, params will be written in the request body. - if (isHttpGET) { + // for HTTP requests that aren't for multipart/form-data, params will be written in the request body. + if (!useMultipartFormData(httpOptions) || useChunkedMode) { val separator = if (urlString.contains("?")) "&" else "?" val newUrl = URL("$urlString$separator$paramString") - return HttpConnectionSetup( - connection = newUrl.openConnection() as HttpURLConnection, - paramStringToWrite = "" - ) - } else { - // TODO move this block elsewhere - /*connection.doOutput = true - connection.outputStream.use { os -> - os.write(paramString.toByteArray()) - }*/ + return newUrl.openConnection() as HttpURLConnection } } - - return HttpConnectionSetup(connection = connection, paramStringToWrite = paramString) + return connection } -} -internal data class HttpConnectionSetup( - val connection: HttpURLConnection, - val paramStringToWrite: String -) \ No newline at end of file + /** + * @return true for any HTTP request that isn't a multipart/form-data + */ + internal fun useMultipartFormData(options: IONFLTRTransferHttpOptions): Boolean = + !options.headers.containsKey("Content-Type") && + (options.method.equals("POST", ignoreCase = true) || options.method.equals("PUT", ignoreCase = true)) + +} From 05dadafb64efb29e59ae1dc53ebe6b9585178323 Mon Sep 17 00:00:00 2001 From: OS-pedrogustavobilro Date: Fri, 9 Jan 2026 09:51:26 +0000 Subject: [PATCH 3/3] chore(release): Prepare for 1.0.3 release. --- CHANGELOG.md | 6 ++++++ pom.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ebec4..b66e7f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.0.3 + +### 2026-01-08 + +- Fix uploading files with params. + ## 1.0.2 ### 2025-12-22 diff --git a/pom.xml b/pom.xml index 9d46eb2..3581ec6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,5 +6,5 @@ 4.0.0 io.ionic.libs ionfiletransfer-android - 1.0.2 + 1.0.3 \ No newline at end of file