Repository: supabase-community/supabase-kt
Pull request #729 (2024-09-16 18:05:20)

Refactor Storage download & upload (flow) implementation
## What kind of change does this PR introduce?

Internal refactor and public API improvements

## What is the current behavior?

The current implementation is very complex/messy and the flow variants are even worse (calling `BucketApiImpl` methods, that shouldn't be in a public extension function).

## What is the new behavior?

### Public API

Every upload and download method now has a `options` DSL. This makes it possible to add configuration options in the future without filling up the parameter list (e.g. file metadata)
For downloads, there are currently only two things to configure:
1. Image transformation (moved down)
2. Http request overrides. This is important for the flow variants to attach download listeners.

Example of the new syntax:
```kotlin
supabase.storage.from("test").downloadAuthenticated("test.jpg") {
    transform {
        size(100, 100)
    }
}
```

For uploads, you can configure:
1. Whether to upsert or not (moved down)
2. The content type (will be inferred if null, wasn't configurable before)
3. Also Http request overrides for flow variants.

Example of the new syntax:
```kotlin
supabase.storage.from("test").upload("test.txt", "Hello World!".encodeToByteArray()) {
    contentType = ContentType.Text.Plain
    upsert = true
}
```

Every upload/update/download variant has this new builder including the flow variants.

`BucketItem` has been renamed to `FileObject` to match the JS lib.

### Internal implementation

For non-flow-variants I cleaned up the code to minimize duplications & improve readability.
The same approach was used for the flow variants, which no longer call internal methods from `BucketApiImpl` and instead use the new `httpOverride` option.

## Additional context

Add any other context or screenshots.


jan-tennert: Note that there are some commits from #694 because I backported and improved some of the changes, but file metadata and `exists`/`info` support is not being added in this PR.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 38296311..d1441205 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -26,4 +26,4 @@ jobs:
           cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
           cache-read-only: false
       - name: Build supabase-kt
-        run: ./gradlew -DLibrariesOnly=true build --stacktrace --configuration-cache --scan
\ No newline at end of file
+        run: ./gradlew -DLibrariesOnly=true build -x test --stacktrace --configuration-cache --scan
diff --git a/README.md b/README.md
index 83416886..ad28e0f5 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ For information about supported Kotlin targets, see the corresponding module REA
 
 [Migrating from version 1.4.X to 2.0.0](/MIGRATION.md)
 
-*Note: The `WASM-JS` target for supported modules is only available for version 2.7.0 and above*
+*Note: The `WASM-JS` target for supported modules is only available for version 3.0.0 and above*
 
 [![](https://img.shields.io/github/release/supabase-community/supabase-kt?label=stable)](https://github.com/supabase-community/supabase-kt/releases) 
 [![](https://badgen.net/github/release/supabase-community/supabase-kt?label=prerelease)](https://central.sonatype.com/search?q=io.github.jan.supabase&smo=true) 
@@ -96,10 +96,10 @@ val iosMain by getting {
 
 **Note:** It is recommended to use the same Ktor version as supabase-kt:
 
-__For 2.7.0 and above:__
+__For 3.0.0 and above:__
 [![Ktor](https://img.shields.io/badge/ktor-3.0.0--rc--1-blue)](https://ktor.io/)
 
-__For versions below 2.7.0:__
+__For versions below 3.0.0:__
 [![Ktor](https://img.shields.io/badge/ktor-2.3.12-blue)](https://ktor.io/)
 
 ## Main Modules
diff --git a/Storage/src/androidAndJvmMain/kotlin/io/github/jan/supabase/storage/JvmUtils.kt b/Storage/src/androidAndJvmMain/kotlin/io/github/jan/supabase/storage/JvmUtils.kt
index 2586f13d..213e319f 100644
--- a/Storage/src/androidAndJvmMain/kotlin/io/github/jan/supabase/storage/JvmUtils.kt
+++ b/Storage/src/androidAndJvmMain/kotlin/io/github/jan/supabase/storage/JvmUtils.kt
@@ -11,163 +11,170 @@ import kotlin.io.path.writeBytes
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return the key to the uploaded file
  */
-suspend fun BucketApi.upload(path: String, file: File, upsert: Boolean = false) = upload(path, UploadData(file.readChannel(), file.length()), upsert)
+suspend fun BucketApi.upload(path: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = upload(path, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.uploadAsFlow(path: String, file: File, upsert: Boolean = false) = uploadAsFlow(path, UploadData(file.readChannel(), file.length()), upsert)
+fun BucketApi.uploadAsFlow(path: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = uploadAsFlow(path, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return the key to the uploaded file
  */
-suspend fun BucketApi.upload(path: String, file: Path, upsert: Boolean = false) = upload(path, UploadData(file.readChannel(), file.fileSize()), upsert)
+suspend fun BucketApi.upload(path: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = upload(path, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.uploadAsFlow(path: String, file: Path, upsert: Boolean = false) = uploadAsFlow(path, UploadData(file.readChannel(), file.fileSize()), upsert)
+fun BucketApi.uploadAsFlow(path: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = uploadAsFlow(path, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return the key to the uploaded file
  */
-suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, file: File, upsert: Boolean = false) = uploadToSignedUrl(path, token, UploadData(file.readChannel(), file.length()), upsert)
+suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrl(path, token, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, file: File, upsert: Boolean = false) = uploadToSignedUrlAsFlow(path, token, UploadData(file.readChannel(), file.length()), upsert)
+fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrlAsFlow(path, token, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  */
-suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, file: Path, upsert: Boolean = false) = uploadToSignedUrl(path, token, UploadData(file.readChannel(), file.fileSize()), upsert)
+suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrl(path, token, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param file The file to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, file: Path, upsert: Boolean = false) = uploadToSignedUrlAsFlow(path, token, UploadData(file.readChannel(), file.fileSize()), upsert)
+fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrlAsFlow(path, token, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to be updated
  * @param file The new file
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  */
-suspend fun BucketApi.update(path: String, file: Path, upsert: Boolean = false) = update(path, UploadData(file.readChannel(), file.fileSize()), upsert)
+suspend fun BucketApi.update(path: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = update(path, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to be updated
  * @param file The new file
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.updateAsFlow(path: String, file: Path, upsert: Boolean = false) = updateAsFlow(path, UploadData(file.readChannel(), file.fileSize()), upsert)
+fun BucketApi.updateAsFlow(path: String, file: Path, options: UploadOptionBuilder.() -> Unit = {}) = updateAsFlow(path, UploadData(file.readChannel(), file.fileSize()), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to be updated
  * @param file The new file
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  */
-suspend fun BucketApi.update(path: String, file: File, upsert: Boolean = false) = update(path, UploadData(file.readChannel(), file.length()), upsert)
+suspend fun BucketApi.update(path: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = update(path, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to be updated
  * @param file The new file
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the uploaded file
  */
-fun BucketApi.updateAsFlow(path: String, file: File, upsert: Boolean = false) = updateAsFlow(path, UploadData(file.readChannel(), file.length()), upsert)
+fun BucketApi.updateAsFlow(path: String, file: File, options: UploadOptionBuilder.() -> Unit = {}) = updateAsFlow(path, UploadData(file.readChannel(), file.length()), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  */
-suspend fun BucketApi.downloadAuthenticatedTo(path: String, file: File, transform: ImageTransformation.() -> Unit = {}) = downloadAuthenticated(path, file.writeChannel(), transform)
+suspend fun BucketApi.downloadAuthenticatedTo(path: String, file: File, options: DownloadOptionBuilder.() -> Unit = {}) = downloadAuthenticated(path, file.writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  * @return A flow that emits the download progress and at last the key to the downloaded file
  */
-fun BucketApi.downloadAuthenticatedToAsFlow(path: String, file: File, transform: ImageTransformation.() -> Unit = {}) = downloadAuthenticatedAsFlow(path, file.writeChannel(), transform)
+fun BucketApi.downloadAuthenticatedToAsFlow(path: String, file: File, options: DownloadOptionBuilder.() -> Unit = {}) = downloadAuthenticatedAsFlow(path, file.writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  */
-suspend fun BucketApi.downloadAuthenticatedTo(path: String, file: Path, transform: ImageTransformation.() -> Unit = {}) = downloadAuthenticated(path, file.toFile().writeChannel(), transform)
+suspend fun BucketApi.downloadAuthenticatedTo(path: String, file: Path, options: DownloadOptionBuilder.() -> Unit = {}) = downloadAuthenticated(path, file.toFile().writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  * @return A flow that emits the download progress and at last the key to the downloaded file
  */
-fun BucketApi.downloadAuthenticatedToAsFlow(path: String, file: Path, transform: ImageTransformation.() -> Unit = {}) = downloadAuthenticatedAsFlow(path, file.toFile().writeChannel(), transform)
+fun BucketApi.downloadAuthenticatedToAsFlow(path: String, file: Path, options: DownloadOptionBuilder.() -> Unit = {}) = downloadAuthenticatedAsFlow(path, file.toFile().writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  */
-suspend fun BucketApi.downloadPublicTo(path: String, file: File, transform: ImageTransformation.() -> Unit = {}) = downloadPublic(path, file.writeChannel(), transform)
+suspend fun BucketApi.downloadPublicTo(path: String, file: File, options: DownloadOptionBuilder.() -> Unit = {}) = downloadPublic(path, file.writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  * @return A flow that emits the download progress and at last the key to the downloaded file
  */
-fun BucketApi.downloadPublicToAsFlow(path: String, file: File, transform: ImageTransformation.() -> Unit = {}) = downloadPublicAsFlow(path, file.writeChannel(), transform)
+fun BucketApi.downloadPublicToAsFlow(path: String, file: File, options: DownloadOptionBuilder.() -> Unit = {}) = downloadPublicAsFlow(path, file.writeChannel(), options)
 
 /**
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  */
-suspend fun BucketApi.downloadPublicTo(path: String, file: Path, transform: ImageTransformation.() -> Unit = {}) {
-    val bytes = downloadPublic(path, transform)
+suspend fun BucketApi.downloadPublicTo(path: String, file: Path, options: DownloadOptionBuilder.() -> Unit = {}) {
+    val bytes = downloadPublic(path, options)
     file.writeBytes(bytes)
 }
 
@@ -175,6 +182,7 @@ suspend fun BucketApi.downloadPublicTo(path: String, file: Path, transform: Imag
  * Downloads a file from [BucketApi.bucketId] under [path] and saves it to [file]
  * @param path The path to download the file from
  * @param file The file to save the data to
+ * @param options Additional options for the download
  * @return A flow that emits the download progress and at last the key to the downloaded file
  */
-fun BucketApi.downloadPublicToAsFlow(path: String, file: Path, transform: ImageTransformation.() -> Unit = {}) = downloadPublicAsFlow(path, file.toFile().writeChannel(), transform)
\ No newline at end of file
+fun BucketApi.downloadPublicToAsFlow(path: String, file: Path, options: DownloadOptionBuilder.() -> Unit = {}) = downloadPublicAsFlow(path, file.toFile().writeChannel(), options)
\ No newline at end of file
diff --git a/Storage/src/androidMain/kotlin/io/github/jan/supabase/storage/AndroidUtils.kt b/Storage/src/androidMain/kotlin/io/github/jan/supabase/storage/AndroidUtils.kt
index 48f883f9..e30c5070 100644
--- a/Storage/src/androidMain/kotlin/io/github/jan/supabase/storage/AndroidUtils.kt
+++ b/Storage/src/androidMain/kotlin/io/github/jan/supabase/storage/AndroidUtils.kt
@@ -9,56 +9,56 @@ import io.ktor.utils.io.jvm.javaio.toByteReadChannel
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param uri The uri to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return the key to the updated file
  */
-suspend fun BucketApi.upload(path: String, uri: Uri, upsert: Boolean = false) = upload(path, UploadData(uri.readChannel(), uri.contentSize), upsert)
+suspend fun BucketApi.upload(path: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = upload(path, UploadData(uri.readChannel(), uri.contentSize), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path]
  * @param path The path to upload the file to
  * @param uri The uri to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the updated file
  */
-fun BucketApi.uploadAsFlow(path: String, uri: Uri, upsert: Boolean = false) = uploadAsFlow(path, UploadData(uri.readChannel(), uri.contentSize), upsert)
+fun BucketApi.uploadAsFlow(path: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = uploadAsFlow(path, UploadData(uri.readChannel(), uri.contentSize), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param uri The uri to upload
  * @return the key to the updated file
  */
-suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, uri: Uri, upsert: Boolean = false) = uploadToSignedUrl(path, token, UploadData(uri.readChannel(), uri.contentSize), upsert)
+suspend fun BucketApi.uploadToSignedUrl(path: String, token: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrl(path, token, UploadData(uri.readChannel(), uri.contentSize), options)
 
 /**
  * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
  * @param path The path to upload the file to
- * @param token The presigned url token
+ * @param token The pre-signed url token
  * @param uri The uri to upload
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the updated file
  */
-fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, uri: Uri, upsert: Boolean = false) = uploadToSignedUrlAsFlow(path, token, UploadData(uri.readChannel(), uri.contentSize), upsert)
+fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = uploadToSignedUrlAsFlow(path, token, UploadData(uri.readChannel(), uri.contentSize), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to update the file to
  * @param uri The uri to update
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return the key to the updated file
  */
-suspend fun BucketApi.update(path: String, uri: Uri, upsert: Boolean = false) = update(path, UploadData(uri.readChannel(), uri.contentSize), upsert)
+suspend fun BucketApi.update(path: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = update(path, UploadData(uri.readChannel(), uri.contentSize), options)
 
 /**
  * Updates a file in [BucketApi.bucketId] under [path]
  * @param path The path to update the file to
  * @param uri The uri to update
- * @param upsert Whether to overwrite an existing file
+ * @param options Additional options for the upload
  * @return A flow that emits the upload progress and at last the key to the updated file
  */
-fun BucketApi.updateAsFlow(path: String, uri: Uri, upsert: Boolean = false) = updateAsFlow(path, UploadData(uri.readChannel(), uri.contentSize), upsert)
+fun BucketApi.updateAsFlow(path: String, uri: Uri, options: UploadOptionBuilder.() -> Unit = {}) = updateAsFlow(path, UploadData(uri.readChannel(), uri.contentSize), options)
 
 @SuppressLint("Recycle") //toByteReadChannel closes the input stream automatically
 private fun Uri.readChannel(): ByteReadChannel {
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt
index a4c7da74..f9f94399 100644
--- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApi.kt
@@ -34,43 +34,47 @@ sealed interface BucketApi {
      * Uploads a file in [bucketId] under [path]
      * @param path The path to upload the file to
      * @param data The data to upload
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key to the uploaded file
      * @throws IllegalArgumentException if data to upload is empty
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun upload(path: String, data: ByteArray, upsert: Boolean = false): FileUploadResponse {
+    suspend fun upload(path: String, data: ByteArray, options: UploadOptionBuilder.() -> Unit = {}): FileUploadResponse {
         require(data.isNotEmpty()) { "The data to upload should not be empty" }
-        return upload(path, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
+        return upload(path, UploadData(ByteReadChannel(data), data.size.toLong()), options)
     }
 
     /**
      * Uploads a file in [bucketId] under [path]
      * @param path The path to upload the file to
      * @param data The data to upload
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key to the uploaded file
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun upload(path: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
+    suspend fun upload(path: String, data: UploadData, options: UploadOptionBuilder.() -> Unit = {}): FileUploadResponse
 
     /**
-     * Uploads a file in [bucketId] under [path] using a presigned url
+     * Uploads a file in [bucketId] under [path] using a pre-signed url
      * @param path The path to upload the file to
-     * @param token The presigned url token
+     * @param token The pre-signed url token
      * @param data The data to upload
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key of the uploaded file
      * @throws IllegalArgumentException if data to upload is empty
      */
-    suspend fun uploadToSignedUrl(path: String, token: String, data: ByteArray, upsert: Boolean = false
+    suspend fun uploadToSignedUrl(
+        path: String,
+        token: String,
+        data: ByteArray,
+        options: UploadOptionBuilder.() -> Unit = {}
     ): FileUploadResponse {
         require(data.isNotEmpty()) { "The data to upload should not be empty" }
-        return uploadToSignedUrl(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
+        return uploadToSignedUrl(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), options)
     }
 
     /**
@@ -78,42 +82,42 @@ sealed interface BucketApi {
      * @param path The path to upload the file to
      * @param token The presigned url token
      * @param data The data to upload
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key of the uploaded file
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      * @throws HttpRequestException on network related issues
      */
-    suspend fun uploadToSignedUrl(path: String, token: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
+    suspend fun uploadToSignedUrl(path: String, token: String, data: UploadData, options: UploadOptionBuilder.() -> Unit = {}): FileUploadResponse
 
     /**
      * Updates a file in [bucketId] under [path]
      * @param path The path to update the file to
      * @param data The new data
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key to the updated file
      * @throws IllegalArgumentException if data to upload is empty
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun update(path: String, data: ByteArray, upsert: Boolean = false): FileUploadResponse {
+    suspend fun update(path: String, data: ByteArray, options: UploadOptionBuilder.() -> Unit = {}): FileUploadResponse {
         require(data.isNotEmpty()) { "The data to upload should not be empty" }
-        return update(path, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
+        return update(path, UploadData(ByteReadChannel(data), data.size.toLong()), options)
     }
 
     /**
      * Updates a file in [bucketId] under [path]
      * @param path The path to update the file to
      * @param data The new data
-     * @param upsert Whether to overwrite an existing file
+     * @param options Additional options for the upload
      * @return the key to the updated file
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun update(path: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
+    suspend fun update(path: String, data: UploadData, options: UploadOptionBuilder.() -> Unit = {}): FileUploadResponse
 
     /**
      * Deletes all files in [bucketId] with in [paths]
@@ -199,51 +203,50 @@ sealed interface BucketApi {
     /**
      * Downloads a file from [bucketId] under [path]
      * @param path The path to download
-     * @param transform The transformation to apply to the image
+     * @param options Additional options for the download
      * @return The file as a byte array
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun downloadAuthenticated(path: String, transform: ImageTransformation.() -> Unit = {}): ByteArray
+    suspend fun downloadAuthenticated(path: String, options: DownloadOptionBuilder.() -> Unit = {}): ByteArray
 
     /**
      * Downloads a file from [bucketId] under [path]
      * @param path The path to download
      * @param channel The channel to write the data to
-     * @param transform The transformation to apply to the image
+     * @param options Additional options for the download
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun downloadAuthenticated(path: String, channel: ByteWriteChannel, transform: ImageTransformation.() -> Unit = {})
+    suspend fun downloadAuthenticated(path: String, channel: ByteWriteChannel, options: DownloadOptionBuilder.() -> Unit = {})
 
     /**
      * Downloads a file from [bucketId] under [path] using the public url
      * @param path The path to download
-     * @param transform The transformation to apply to the image
+     * @param options Additional options for the download
      * @return The file as a byte array
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun downloadPublic(path: String, transform: ImageTransformation.() -> Unit = {}): ByteArray
+    suspend fun downloadPublic(path: String, options: DownloadOptionBuilder.() -> Unit = {}): ByteArray
 
     /**
      * Downloads a file from [bucketId] under [path] using the public url
      * @param path The path to download
      * @param channel The channel to write the data to
-     * @param transform The transformation to apply to the image
+     * @param options Additional options for the download
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
      */
-    suspend fun downloadPublic(path: String, channel: ByteWriteChannel, transform: ImageTransformation.() -> Unit = {})
+    suspend fun downloadPublic(path: String, channel: ByteWriteChannel, options: DownloadOptionBuilder.() -> Unit = {})
 
 
     /**
-     * Searches for buckets with the given [prefix] and [filter]
-     * @return The filtered buckets
+     * Searches for files with the given [prefix] and [filter]
      * @throws RestException or one of its subclasses if receiving an error response
      * @throws HttpRequestTimeoutException if the request timed out
      * @throws HttpRequestException on network related issues
@@ -251,7 +254,7 @@ sealed interface BucketApi {
     suspend fun list(
         prefix: String = "",
         filter: BucketListFilter.() -> Unit = {}
-    ): List<BucketItem>
+    ): List<FileObject>
 
     /**
      * Changes the bucket's public status to [public]
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApiImpl.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApiImpl.kt
index f0fb4ed4..986f2582 100644
--- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApiImpl.kt
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketApiImpl.kt
@@ -8,7 +8,6 @@ import io.github.jan.supabase.storage.resumable.ResumableClientImpl
 import io.ktor.client.call.body
 import io.ktor.client.request.HttpRequestBuilder
 import io.ktor.client.request.header
-import io.ktor.client.request.parameter
 import io.ktor.client.request.setBody
 import io.ktor.client.request.url
 import io.ktor.client.statement.bodyAsChannel
@@ -36,18 +35,24 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
 
     override val resumable = ResumableClientImpl(this, resumableCache)
 
-    override suspend fun update(path: String, data: UploadData, upsert: Boolean): FileUploadResponse =
+    override suspend fun update(
+        path: String,
+        data: UploadData,
+        options: UploadOptionBuilder.() -> Unit
+    ): FileUploadResponse =
         uploadOrUpdate(
-            HttpMethod.Put, bucketId, path, data, upsert
+            HttpMethod.Put, defaultUploadUrl(path), data, options
         )
 
     override suspend fun uploadToSignedUrl(
         path: String,
         token: String,
         data: UploadData,
-        upsert: Boolean
+        options: UploadOptionBuilder.() -> Unit
     ): FileUploadResponse {
-        return uploadToSignedUrl(path, token, data, upsert) {}
+        return uploadOrUpdate(
+            HttpMethod.Put, uploadToSignedUrlUrl(path, token), data, options
+        )
     }
 
     override suspend fun createSignedUploadUrl(path: String): UploadSignedUrl {
@@ -63,9 +68,13 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
         )
     }
 
-    override suspend fun upload(path: String, data: UploadData, upsert: Boolean): FileUploadResponse =
+    override suspend fun upload(
+        path: String,
+        data: UploadData,
+        options: UploadOptionBuilder.() -> Unit
+    ): FileUploadResponse =
         uploadOrUpdate(
-            HttpMethod.Post, bucketId, path, data, upsert
+            HttpMethod.Post, defaultUploadUrl(path), data, options
         )
 
     override suspend fun delete(paths: Collection<String>) {
@@ -127,51 +136,54 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
 
     override suspend fun downloadAuthenticated(
         path: String,
-        transform: ImageTransformation.() -> Unit
-    ): ByteArray {
-        return storage.api.rawRequest {
-            prepareDownloadRequest(path, false, transform)
-        }.body()
-    }
+        options: DownloadOptionBuilder.() -> Unit
+    ): ByteArray = normalDownloadRequest(path, false, options)
 
 
     override suspend fun downloadPublic(
         path: String,
-        transform: ImageTransformation.() -> Unit
+        options: DownloadOptionBuilder.() -> Unit
+    ): ByteArray = normalDownloadRequest(path, true, options)
+
+    private suspend fun normalDownloadRequest(
+        path: String,
+        public: Boolean,
+        options: DownloadOptionBuilder.() -> Unit
     ): ByteArray {
+        val downloadOptions = DownloadOptionBuilder().apply(options)
         return storage.api.rawRequest {
-            prepareDownloadRequest(path, true, transform)
+            prepareDownloadRequest(path, public, downloadOptions)
+            downloadOptions.httpRequestOverrides.forEach { it() }
         }.body()
     }
 
-
     override suspend fun downloadAuthenticated(
         path: String,
         channel: ByteWriteChannel,
-        transform: ImageTransformation.() -> Unit
+        options: DownloadOptionBuilder.() -> Unit
     ) {
-        channelDownloadRequest(path, channel, false, transform)
+        channelDownloadRequest(path, channel, false, options)
     }
 
 
     override suspend fun downloadPublic(
         path: String,
         channel: ByteWriteChannel,
-        transform: ImageTransformation.() -> Unit
+        options: DownloadOptionBuilder.() -> Unit
     ) {
-        channelDownloadRequest(path, channel, true, transform)
+        channelDownloadRequest(path, channel, true, options)
     }
 
     internal suspend fun channelDownloadRequest(
         path: String,
         channel: ByteWriteChannel,
         public: Boolean,
-        transform: ImageTransformation.() -> Unit,
-        extra: HttpRequestBuilder.() -> Unit = {}
+        options: DownloadOptionBuilder.() -> Unit,
     ) {
+        val downloadOptions = DownloadOptionBuilder().apply(options)
         storage.api.prepareRequest {
-            prepareDownloadRequest(path, public, transform)
-            extra()
+            prepareDownloadRequest(path, public, downloadOptions)
+            downloadOptions.httpRequestOverrides.forEach { it() }
         }.execute {
             it.bodyAsChannel().copyTo(channel)
         }
@@ -181,18 +193,17 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
     internal fun HttpRequestBuilder.prepareDownloadRequest(
         path: String,
         public: Boolean,
-        transform: ImageTransformation.() -> Unit
+        options: DownloadOptionBuilder
     ) {
-        val transformation = ImageTransformation().apply(transform).queryString()
+        val transformation = ImageTransformation().apply(options.transform).queryString()
         val url = when (public) {
             true -> if (transformation.isBlank()) publicUrl(path) else publicRenderUrl(
                 path,
-                transform
+                options.transform
             )
-
             false -> if (transformation.isBlank()) authenticatedUrl(path) else authenticatedRenderUrl(
                 path,
-                transform
+                options.transform
             )
         }
         method = HttpMethod.Get
@@ -202,32 +213,29 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
     override suspend fun list(
         prefix: String,
         filter: BucketListFilter.() -> Unit
-    ): List<BucketItem> {
+    ): List<FileObject> {
         return storage.api.postJson("object/list/$bucketId", buildJsonObject {
             put("prefix", prefix)
             putJsonObject(BucketListFilter().apply(filter).build())
         }).safeBody()
     }
 
-    @Suppress("LongParameterList") //TODO: maybe refactor
+    private fun defaultUploadUrl(path: String) = "object/$bucketId/$path"
+
+    private fun uploadToSignedUrlUrl(path: String, token: String) = "object/upload/sign/$bucketId/$path?token=$token"
+
     internal suspend fun uploadOrUpdate(
         method: HttpMethod,
-        bucket: String,
-        path: String,
+        url: String,
         data: UploadData,
-        upsert: Boolean,
-        extra: HttpRequestBuilder.() -> Unit = {}
+        options: UploadOptionBuilder.() -> Unit,
     ): FileUploadResponse {
-        val response = storage.api.request("object/$bucket/$path") {
+        val path = url.substringAfterLast('/').substringBeforeLast("?")
+        val optionBuilder = UploadOptionBuilder(storage.serializer).apply(options)
+        val response = storage.api.request(url) {
             this.method = method
-            setBody(object : OutgoingContent.ReadChannelContent() {
-                override val contentType: ContentType = ContentType.defaultForFilePath(path)
-                override val contentLength: Long = data.size
-                override fun readFrom(): ByteReadChannel = data.stream
-            })
-            header(HttpHeaders.ContentType, ContentType.defaultForFilePath(path))
-            header(UPSERT_HEADER, upsert.toString())
-            extra()
+            defaultUploadRequest(path, data, optionBuilder)
+            optionBuilder.httpRequestOverrides.forEach { it() }
         }.body<JsonObject>()
         val key = response["Key"]?.jsonPrimitive?.content
             ?: error("Expected a key in a upload response")
@@ -236,28 +244,18 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
         return FileUploadResponse(id, path, key)
     }
 
-    internal suspend fun uploadToSignedUrl(
+    private fun HttpRequestBuilder.defaultUploadRequest(
         path: String,
-        token: String,
         data: UploadData,
-        upsert: Boolean,
-        extra: HttpRequestBuilder.() -> Unit = {}
-    ): FileUploadResponse {
-        val response = storage.api.put("object/upload/sign/$bucketId/$path") {
-            parameter("token", token)
-            setBody(object : OutgoingContent.ReadChannelContent() {
-                override val contentType: ContentType = ContentType.defaultForFilePath(path)
-                override val contentLength: Long = data.size
-                override fun readFrom(): ByteReadChannel = data.stream
-            })
-            header(HttpHeaders.ContentType, ContentType.defaultForFilePath(path))
-            header("x-upsert", upsert.toString())
-            extra()
-        }.body<JsonObject>()
-        val key = response["Key"]?.jsonPrimitive?.content
-            ?: error("Expected a key in a upload response")
-        val id = response["Id"]?.jsonPrimitive?.content ?: error("Expected an id in a upload response")
-        return FileUploadResponse(id, path, key)
+        optionBuilder: UploadOptionBuilder,
+    ) {
+        setBody(object : OutgoingContent.ReadChannelContent() {
+            override val contentType: ContentType = optionBuilder.contentType ?: ContentType.defaultForFilePath(path)
+            override val contentLength: Long = data.size
+            override fun readFrom(): ByteReadChannel = data.stream
+        })
+        header(HttpHeaders.ContentType, optionBuilder.contentType ?: ContentType.defaultForFilePath(path))
+        header(UPSERT_HEADER, optionBuilder.upsert.toString())
     }
 
     override suspend fun changePublicStatusTo(public: Boolean) = storage.updateBucket(bucketId) {
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/DownloadOptionBuilder.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/DownloadOptionBuilder.kt
new file mode 100644
index 00000000..9110f7d7
--- /dev/null
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/DownloadOptionBuilder.kt
@@ -0,0 +1,29 @@
+package io.github.jan.supabase.storage
+
+import io.github.jan.supabase.network.HttpRequestOverride
+
+/**
+ * Builder for downloading files with additional options
+ */
+class DownloadOptionBuilder(
+    internal var transform: ImageTransformation.() -> Unit = {},
+    internal val httpRequestOverrides: MutableList<HttpRequestOverride> = mutableListOf()
+) {
+
+    /**
+     * Transforms the image before downloading
+     * @param transform The transformation to apply
+     */
+    fun transform(transform: ImageTransformation.() -> Unit) {
+        this.transform = transform
+    }
+
+    /**
+     * Overrides the HTTP request
+     * @param override The override to apply
+     */
+    fun httpOverride(override: HttpRequestOverride) {
+        httpRequestOverrides.add(override)
+    }
+
+}
\ No newline at end of file
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketItem.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FileObject.kt
similarity index 97%
rename from Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketItem.kt
rename to Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FileObject.kt
index 595eb8d9..8caf02a8 100644
--- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/BucketItem.kt
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FileObject.kt
@@ -15,7 +15,7 @@ import kotlinx.serialization.json.JsonObject
  * @param metadata The metadata of the item
  */
 @Serializable
-data class BucketItem(
+data class FileObject(
     val name: String,
     val id: String?,
     @SerialName("updated_at")
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExt.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExt.kt
deleted file mode 100644
index 728b5e77..00000000
--- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExt.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-package io.github.jan.supabase.storage
-
-import io.github.jan.supabase.exceptions.HttpRequestException
-import io.github.jan.supabase.exceptions.RestException
-import io.ktor.client.call.body
-import io.ktor.client.plugins.HttpRequestTimeoutException
-import io.ktor.client.plugins.onDownload
-import io.ktor.client.plugins.onUpload
-import io.ktor.http.HttpMethod
-import io.ktor.utils.io.ByteReadChannel
-import io.ktor.utils.io.ByteWriteChannel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-
-/**
- * Uploads a file in [bucketId] under [path]
- * @param path The path to upload the file to
- * @param upsert Whether to overwrite an existing file
- * @param data The data to upload
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.updateAsFlow(path: String, data: UploadData, upsert: Boolean = false): Flow<UploadStatus> = callbackFlow {
-    this@updateAsFlow as BucketApiImpl
-    val key = uploadOrUpdate(HttpMethod.Put, bucketId, path, data, upsert) {
-        onUpload { bytesSentTotal, contentLength ->
-            trySend(UploadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-        }
-    }
-    trySend(UploadStatus.Success(key))
-    close()
-}
-
-/**
- * Uploads a file in [bucketId] under [path]
- * @param path The path to upload the file to
- * @param upsert Whether to overwrite an existing file
- * @param data The data to upload
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.uploadAsFlow(path: String, data: ByteArray, upsert: Boolean = false): Flow<UploadStatus> = uploadAsFlow(path, UploadData(
-    ByteReadChannel(data), data.size.toLong()), upsert)
-
-/**
- * Uploads a file in [bucketId] under [path] using a presigned url
- * @param path The path to upload the file to
- * @param token The presigned url token
- * @param data The data to upload
- * @param upsert Whether to overwrite an existing file
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.uploadToSignedUrlAsFlow(
-    path: String,
-    token: String,
-    data: UploadData,
-    upsert: Boolean = false
-): Flow<UploadStatus> {
-    return callbackFlow {
-        this@uploadToSignedUrlAsFlow as BucketApiImpl
-        val key = uploadToSignedUrl(path, token, data, upsert) {
-            onUpload { bytesSentTotal, contentLength ->
-                trySend(UploadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-            }
-        }
-        trySend(UploadStatus.Success(key))
-        close()
-    }
-}
-
-/**
- * Uploads a file in [bucketId] under [path] using a presigned url
- * @param path The path to upload the file to
- * @param token The presigned url token
- * @param data The data to upload
- * @param upsert Whether to overwrite an existing file
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- */
-fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, data: ByteArray, upsert: Boolean = false): Flow<UploadStatus> = uploadToSignedUrlAsFlow(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
-
-/**
- * Updates a file in [bucketId] under [path]
- * @param path The path to update the file to
- * @param data The new data
- * @param upsert Whether to overwrite an existing file
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.uploadAsFlow(path: String, data: UploadData, upsert: Boolean = false): Flow<UploadStatus> {
-    return callbackFlow {
-        this@uploadAsFlow as BucketApiImpl
-        val key = uploadOrUpdate(HttpMethod.Post, bucketId, path, data, upsert) {
-            onUpload { bytesSentTotal, contentLength ->
-                trySend(UploadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-            }
-        }
-        trySend(UploadStatus.Success(key))
-        close()
-    }
-}
-
-/**
- * Updates a file in [bucketId] under [path]
- * @param path The path to update the file to
- * @param data The new data
- * @param upsert Whether to overwrite an existing file
- * @return A flow that emits the upload progress and at last the key to the uploaded file
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.updateAsFlow(path: String, data: ByteArray, upsert: Boolean = false): Flow<UploadStatus> = updateAsFlow(path, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
-
-/**
- * Downloads a file from [bucketId] under [path]
- * @param path The path to download
- * @param transform The transformation to apply to the image
- * @return A flow that emits the download progress and at last the data as a byte array
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.downloadAuthenticatedAsFlow(
-    path: String,
-    transform: ImageTransformation.() -> Unit = {}
-): Flow<DownloadStatus> {
-    return callbackFlow {
-        this@downloadAuthenticatedAsFlow as BucketApiImpl
-        val data = storage.api.rawRequest {
-            prepareDownloadRequest(path, false, transform)
-            onDownload { bytesSentTotal, contentLength ->
-                trySend(DownloadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-            }
-        }.body<ByteArray>()
-        trySend(DownloadStatus.Success)
-        trySend(DownloadStatus.ByteData(data))
-        close()
-    }
-}
-
-/**
- * Downloads a file from [bucketId] under [path]
- * @param path The path to download
- * @param channel The channel to write the data to
- * @param transform The transformation to apply to the image
- * @return A flow that emits the download progress and at last the data as a byte array
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.downloadAuthenticatedAsFlow(
-    path: String,
-    channel: ByteWriteChannel,
-    transform: ImageTransformation.() -> Unit = {}
-): Flow<DownloadStatus> {
-    this as BucketApiImpl
-    return flowChannelDownloadRequest(path, channel, false, transform)
-}
-
-/**
- * Downloads a file from [bucketId] under [path] using the public url
- * @param path The path to download
- * @param transform The transformation to apply to the image
- * @return A flow that emits the download progress and at last the data as a byte array
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.downloadPublicAsFlow(path: String, transform: ImageTransformation.() -> Unit = {}): Flow<DownloadStatus> {
-    return callbackFlow {
-        this@downloadPublicAsFlow as BucketApiImpl
-        val data = storage.api.rawRequest {
-            prepareDownloadRequest(path, true, transform)
-            onDownload { bytesSentTotal, contentLength ->
-                trySend(DownloadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-            }
-        }.body<ByteArray>()
-        trySend(DownloadStatus.Success)
-        trySend(DownloadStatus.ByteData(data))
-        close()
-    }
-}
-
-/**
- * Downloads a file from [bucketId] under [path] using the public url
- * @param path The path to download
- * @param channel The channel to write the data to
- * @param transform The transformation to apply to the image
- * @return A flow that emits the download progress and at last the data as a byte array
- * @throws RestException or one of its subclasses if receiving an error response
- * @throws HttpRequestTimeoutException if the request timed out
- * @throws HttpRequestException on network related issues
- */
-fun BucketApi.downloadPublicAsFlow(
-    path: String,
-    channel: ByteWriteChannel,
-    transform: ImageTransformation.() -> Unit = {}
-): Flow<DownloadStatus> {
-    this as BucketApiImpl
-    return flowChannelDownloadRequest(path, channel, true, transform)
-}
-
-private fun BucketApiImpl.flowChannelDownloadRequest(
-    path: String,
-    channel: ByteWriteChannel,
-    public: Boolean,
-    transform: ImageTransformation.() -> Unit
-): Flow<DownloadStatus> = callbackFlow {
-    channelDownloadRequest(path, channel, public, transform) {
-        onDownload { bytesSentTotal, contentLength ->
-            trySend(DownloadStatus.Progress(bytesSentTotal, contentLength ?: 0))
-        }
-    }
-    trySend(DownloadStatus.Success)
-    close()
-}
\ No newline at end of file
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExtension.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExtension.kt
new file mode 100644
index 00000000..146b0bbb
--- /dev/null
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/FlowExtension.kt
@@ -0,0 +1,229 @@
+package io.github.jan.supabase.storage
+
+import io.github.jan.supabase.exceptions.HttpRequestException
+import io.github.jan.supabase.exceptions.RestException
+import io.github.jan.supabase.network.HttpRequestOverride
+import io.ktor.client.plugins.HttpRequestTimeoutException
+import io.ktor.client.plugins.onDownload
+import io.ktor.client.plugins.onUpload
+import io.ktor.utils.io.ByteReadChannel
+import io.ktor.utils.io.ByteWriteChannel
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+private fun downloadOverride(flowProducer: ProducerScope<DownloadStatus>): HttpRequestOverride = {
+    onDownload { bytesSentTotal, contentLength ->
+        flowProducer.trySend(DownloadStatus.Progress(bytesSentTotal, contentLength ?: 0))
+    }
+}
+
+private fun uploadOverride(flowProducer: ProducerScope<UploadStatus>): HttpRequestOverride = {
+    onUpload { bytesSentTotal, contentLength ->
+        flowProducer.trySend(UploadStatus.Progress(bytesSentTotal, contentLength ?: 0))
+    }
+}
+
+/**
+ * Uploads a file in [BucketApi.bucketId] under [path]
+ * @param path The path to upload the file to
+ * @param options Additional options for the upload
+ * @param data The data to upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.updateAsFlow(
+    path: String,
+    data: UploadData,
+    options: UploadOptionBuilder.() -> Unit = {}
+): Flow<UploadStatus> = uploadAsFlowRequest {
+    update(path, data) {
+        options()
+        httpOverride(it)
+    }
+}
+
+/**
+ * Uploads a file in [BucketApi.bucketId] under [path]
+ * @param path The path to upload the file to
+ * @param options Additional options for the upload
+ * @param data The data to upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.uploadAsFlow(path: String, data: ByteArray, options: UploadOptionBuilder.() -> Unit = {}): Flow<UploadStatus> = uploadAsFlow(path, UploadData(
+    ByteReadChannel(data), data.size.toLong()), options)
+
+/**
+ * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
+ * @param path The path to upload the file to
+ * @param token The presigned url token
+ * @param data The data to upload
+ * @param options Additional options for the upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.uploadToSignedUrlAsFlow(
+    path: String,
+    token: String,
+    data: UploadData,
+    options: UploadOptionBuilder.() -> Unit = {}
+): Flow<UploadStatus> = uploadAsFlowRequest {
+    uploadToSignedUrl(path, token, data) {
+        options()
+        httpOverride(it)
+    }
+}
+
+/**
+ * Uploads a file in [BucketApi.bucketId] under [path] using a presigned url
+ * @param path The path to upload the file to
+ * @param token The presigned url token
+ * @param data The data to upload
+ * @param options Additional options for the upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ */
+fun BucketApi.uploadToSignedUrlAsFlow(path: String, token: String, data: ByteArray, options: UploadOptionBuilder.() -> Unit = {}): Flow<UploadStatus> = uploadToSignedUrlAsFlow(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), options)
+
+/**
+ * Updates a file in [BucketApi.bucketId] under [path]
+ * @param path The path to update the file to
+ * @param data The new data
+ * @param options Additional options for the upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.uploadAsFlow(
+    path: String,
+    data: UploadData,
+    options: UploadOptionBuilder.() -> Unit = {}
+): Flow<UploadStatus> = uploadAsFlowRequest {
+    upload(path, data) {
+        options()
+        httpOverride(it)
+    }
+}
+
+/**
+ * Updates a file in [BucketApi.bucketId] under [path]
+ * @param path The path to update the file to
+ * @param data The new data
+ * @param options Additional options for the upload
+ * @return A flow that emits the upload progress and at last the key to the uploaded file
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.updateAsFlow(path: String, data: ByteArray, options: UploadOptionBuilder.() -> Unit = {}): Flow<UploadStatus> = updateAsFlow(path, UploadData(ByteReadChannel(data), data.size.toLong()), options)
+
+private fun BucketApi.uploadAsFlowRequest(
+    producer: suspend (HttpRequestOverride) -> FileUploadResponse
+) = callbackFlow {
+    val key = producer(uploadOverride(this@callbackFlow))
+    trySend(UploadStatus.Success(key))
+    close()
+}
+
+/**
+ * Downloads a file from [BucketApi.bucketId] under [path]
+ * @param path The path to download
+ * @param options Additional options for the download
+ * @return A flow that emits the download progress and at last the data as a byte array
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.downloadAuthenticatedAsFlow(
+    path: String,
+    options: DownloadOptionBuilder.() -> Unit = {}
+): Flow<DownloadStatus> = downloadAsFlowRequest {
+    downloadAuthenticated(path) {
+        options()
+        httpOverride(it)
+    }
+}
+
+/**
+ * Downloads a file from [BucketApi.bucketId] under [path] using the public url
+ * @param path The path to download
+ * @param options Additional options for the download
+ * @return A flow that emits the download progress and at last the data as a byte array
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.downloadPublicAsFlow(
+    path: String,
+    options: DownloadOptionBuilder.() -> Unit = {}
+): Flow<DownloadStatus> =
+    downloadAsFlowRequest {
+        downloadPublic(path) {
+            options()
+            httpOverride(it)
+        }
+    }
+
+
+/**
+ * Downloads a file from [BucketApi.bucketId] under [path]
+ * @param path The path to download
+ * @param channel The channel to write the data to
+ * @param options Additional options for the download
+ * @return A flow that emits the download progress and at last the data as a byte array
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.downloadAuthenticatedAsFlow(
+    path: String,
+    channel: ByteWriteChannel,
+    options: DownloadOptionBuilder.() -> Unit = {}
+): Flow<DownloadStatus> = downloadAsFlowRequest {
+    downloadAuthenticated(path, channel) {
+        options()
+        httpOverride(it)
+    }
+    null
+}
+
+/**
+ * Downloads a file from [BucketApi.bucketId] under [path] using the public url
+ * @param path The path to download
+ * @param channel The channel to write the data to
+ * @param options Additional options for the download
+ * @return A flow that emits the download progress and at last the data as a byte array
+ * @throws RestException or one of its subclasses if receiving an error response
+ * @throws HttpRequestTimeoutException if the request timed out
+ * @throws HttpRequestException on network related issues
+ */
+fun BucketApi.downloadPublicAsFlow(
+    path: String,
+    channel: ByteWriteChannel,
+    options: DownloadOptionBuilder.() -> Unit = {}
+): Flow<DownloadStatus> = downloadAsFlowRequest {
+    downloadPublic(path, channel) {
+        options()
+        httpOverride(it)
+    }
+    null
+}
+
+private fun BucketApi.downloadAsFlowRequest(
+    producer: suspend (HttpRequestOverride) -> ByteArray?
+) = callbackFlow {
+    //If null, the data gets streamed to a channel, so we don't need to emit it later
+    val data = producer(downloadOverride(this@callbackFlow))
+    trySend(DownloadStatus.Success)
+    if (data != null) {
+        trySend(DownloadStatus.ByteData(data))
+    }
+    close()
+}
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt
index 64eea8f3..8e106f4d 100644
--- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt
@@ -1,6 +1,7 @@
 package io.github.jan.supabase.storage
 
 import io.github.jan.supabase.SupabaseClient
+import io.github.jan.supabase.SupabaseSerializer
 import io.github.jan.supabase.annotations.SupabaseInternal
 import io.github.jan.supabase.bodyOrNull
 import io.github.jan.supabase.collections.AtomicMutableMap
@@ -13,6 +14,8 @@ import io.github.jan.supabase.exceptions.UnknownRestException
 import io.github.jan.supabase.gotrue.authenticatedSupabaseApi
 import io.github.jan.supabase.logging.SupabaseLogger
 import io.github.jan.supabase.logging.w
+import io.github.jan.supabase.plugins.CustomSerializationConfig
+import io.github.jan.supabase.plugins.CustomSerializationPlugin
 import io.github.jan.supabase.plugins.MainConfig
 import io.github.jan.supabase.plugins.MainPlugin
 import io.github.jan.supabase.plugins.SupabasePluginProvider
@@ -46,7 +49,7 @@ import kotlin.time.Duration.Companion.seconds
  * val bytes = bucket.downloadAuthenticated("icon.png")
  * ```
  */
-sealed interface Storage : MainPlugin<Storage.Config> {
+sealed interface Storage : MainPlugin<Storage.Config>, CustomSerializationPlugin {
 
     /**
      * Creates a new bucket in the storage
@@ -116,8 +119,9 @@ sealed interface Storage : MainPlugin<Storage.Config> {
      */
     data class Config(
         var transferTimeout: Duration = 120.seconds,
-        @PublishedApi internal var resumable: Resumable = Resumable()
-    ) : MainConfig() {
+        @PublishedApi internal var resumable: Resumable = Resumable(),
+        override var serializer: SupabaseSerializer? = null
+    ) : MainConfig(), CustomSerializationConfig {
 
         /**
          * @param cache the cache for caching resumable upload urls
@@ -185,6 +189,8 @@ internal class StorageImpl(override val supabaseClient: SupabaseClient, override
     override val apiVersion: Int
         get() = Storage.API_VERSION
 
+    override val serializer: SupabaseSerializer = config.serializer ?: supabaseClient.defaultSerializer
+
     @OptIn(SupabaseInternal::class)
     internal val api = supabaseClient.authenticatedSupabaseApi(this) {
         timeout {
diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/UploadOptionBuilder.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/UploadOptionBuilder.kt
new file mode 100644
index 00000000..61bfa326
--- /dev/null
+++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/UploadOptionBuilder.kt
@@ -0,0 +1,27 @@
+package io.github.jan.supabase.storage
+
+import io.github.jan.supabase.SupabaseSerializer
+import io.github.jan.supabase.network.HttpRequestOverride
+import io.ktor.http.ContentType
+
+/**
+ * Builder for uploading files with additional options
+ * @param serializer The serializer to use for encoding the metadata
+ * @param upsert Whether to update the file if it already exists
+ * @param contentType The content type of the file. If null, the content type will be inferred from the file extension
+ */
+class UploadOptionBuilder(
+    @PublishedApi internal val serializer: SupabaseSerializer,
+    var upsert: Boolean = false,
+    var contentType: ContentType? = null,
+    internal val httpRequestOverrides: MutableList<HttpRequestOverride> = mutableListOf()
+) {
+
+    /**
+     * Overrides the HTTP request
+     */
+    fun httpOverride(override: HttpRequestOverride) {
+        httpRequestOverrides.add(override)
+    }
+
+}
diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt
index a46079bf..b86c9bf2 100644
--- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt
+++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt
@@ -29,6 +29,11 @@ import kotlin.time.Duration.Companion.milliseconds
 
 private const val HTTPS_PORT = 443
 
+/**
+ * A function that can be used to override the default request configuration
+ */
+typealias HttpRequestOverride = HttpRequestBuilder.() -> Unit
+
 /**
  * A [SupabaseHttpClient] that uses ktor to send requests
  */


Test patch:
diff --git a/Storage/src/commonTest/kotlin/BucketApiFlowTest.kt b/Storage/src/commonTest/kotlin/BucketApiFlowTest.kt
index c0eb5757..939a7306 100644
--- a/Storage/src/commonTest/kotlin/BucketApiFlowTest.kt
+++ b/Storage/src/commonTest/kotlin/BucketApiFlowTest.kt
@@ -73,7 +73,9 @@ class BucketApiFlowTest {
     @Test
     fun testUpsertAsFlowMethodWithByteArray() {
         testUploadAsFlow(upsert = true) { client, expectedPath, data ->
-            val flow = client.storage[bucketId].uploadAsFlow(expectedPath, data, upsert = true)
+            val flow = client.storage[bucketId].uploadAsFlow(expectedPath, data) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, data)
         }
     }
@@ -95,7 +97,9 @@ class BucketApiFlowTest {
             val channel = ByteReadChannel(bytes)
             val expectedSize = bytes.size.toLong()
             val data = UploadData(channel, expectedSize)
-            val flow = client.storage[bucketId].uploadAsFlow(expectedPath, data, upsert = true)
+            val flow = client.storage[bucketId].uploadAsFlow(expectedPath, data) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, bytes)
         }
     }
@@ -131,7 +135,9 @@ class BucketApiFlowTest {
     @Test
     fun testUpdateUpsertAsFlowMethodWithByteArray() {
         testUpdateAsFlow(upsert = true) { client, expectedPath, data ->
-            val flow = client.storage[bucketId].updateAsFlow(expectedPath, data, upsert = true)
+            val flow = client.storage[bucketId].updateAsFlow(expectedPath, data) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, data)
         }
     }
@@ -153,7 +159,9 @@ class BucketApiFlowTest {
             val channel = ByteReadChannel(bytes)
             val expectedSize = bytes.size.toLong()
             val data = UploadData(channel, expectedSize)
-            val flow = client.storage[bucketId].updateAsFlow(expectedPath, data, upsert = true)
+            val flow = client.storage[bucketId].updateAsFlow(expectedPath, data) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, bytes)
         }
     }
@@ -206,7 +214,9 @@ class BucketApiFlowTest {
             val channel = ByteReadChannel(bytes)
             val expectedSize = bytes.size.toLong()
             val data = UploadData(channel, expectedSize)
-            val flow = client.storage[bucketId].uploadToSignedUrlAsFlow(expectedPath, expectedToken, data, true)
+            val flow = client.storage[bucketId].uploadToSignedUrlAsFlow(expectedPath, expectedToken, data) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, bytes)
         }
     }
@@ -214,7 +224,9 @@ class BucketApiFlowTest {
     @Test
     fun testUpsertToSignedUrlAsFlowMethodByteArray() {
         testUploadToSignedUrl(upsert = true) { client, expectedPath, bytes ->
-            val flow = client.storage[bucketId].uploadToSignedUrlAsFlow(expectedPath, expectedToken, bytes, true)
+            val flow = client.storage[bucketId].uploadToSignedUrlAsFlow(expectedPath, expectedToken, bytes) {
+                upsert = true
+            }
             testUploadFlowWithByteArray(flow, expectedPath, bytes)
         }
     }
diff --git a/Storage/src/commonTest/kotlin/BucketApiTest.kt b/Storage/src/commonTest/kotlin/BucketApiTest.kt
index b5e56849..ece06524 100644
--- a/Storage/src/commonTest/kotlin/BucketApiTest.kt
+++ b/Storage/src/commonTest/kotlin/BucketApiTest.kt
@@ -21,11 +21,14 @@ import io.ktor.http.HttpMethod
 import io.ktor.http.headersOf
 import kotlinx.coroutines.test.runTest
 import kotlinx.datetime.Clock
+import kotlinx.serialization.json.buildJsonObject
 import kotlinx.serialization.json.int
 import kotlinx.serialization.json.jsonArray
 import kotlinx.serialization.json.jsonObject
 import kotlinx.serialization.json.jsonPrimitive
 import kotlinx.serialization.json.long
+import kotlinx.serialization.json.put
+import kotlin.io.encoding.ExperimentalEncodingApi
 import kotlin.test.Test
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
@@ -74,7 +77,9 @@ class BucketApiTest {
                 )
             },
             request = { client, expectedPath, data ->
-                client.storage[bucketId].upload(expectedPath, data, upsert = true)
+                client.storage[bucketId].upload(expectedPath, data) {
+                    upsert = true
+                }
             }
         )
     }
@@ -98,7 +103,9 @@ class BucketApiTest {
                 )
             },
             request = { client, expectedPath, data ->
-                client.storage[bucketId].uploadToSignedUrl(path = expectedPath, token = expectedToken, data = data, upsert = false)
+                client.storage[bucketId].uploadToSignedUrl(path = expectedPath, token = expectedToken, data = data) {
+                    upsert = false
+                }
             }
         )
     }
@@ -122,7 +129,9 @@ class BucketApiTest {
                 )
             },
             request = { client, expectedPath, data ->
-                client.storage[bucketId].uploadToSignedUrl(path = expectedPath, token = expectedToken, data = data, upsert = true)
+                client.storage[bucketId].uploadToSignedUrl(path = expectedPath, token = expectedToken, data = data) {
+                    upsert = true
+                }
             }
         )
     }
@@ -158,7 +167,9 @@ class BucketApiTest {
                 )
             },
             request = { client, expectedPath, data ->
-                client.storage[bucketId].update(expectedPath, data, upsert = true)
+                client.storage[bucketId].update(expectedPath, data) {
+                    upsert = true
+                }
             }
         )
     }
@@ -458,11 +469,12 @@ class BucketApiTest {
                 quality = expectedQuality
                 resize = expectedResize
             }
-            val data = if(authenticated) client.storage[bucketId].downloadAuthenticated(expectedPath, transform) else client.storage[bucketId].downloadPublic(expectedPath, transform)
+            val data = if(authenticated) client.storage[bucketId].downloadAuthenticated(expectedPath) { transform(transform) } else client.storage[bucketId].downloadPublic(expectedPath)  { transform(transform) }
             assertContentEquals(expectedData, data, "Data should be [1, 2, 3]")
         }
     }
 
+    @OptIn(ExperimentalEncodingApi::class)
     private fun testUploadMethod(
         method: HttpMethod,
         urlPath: String,
@@ -472,6 +484,9 @@ class BucketApiTest {
     ) {
         runTest {
             val expectedData = byteArrayOf(1, 2, 3)
+            val expectedMetadata = buildJsonObject {
+                put("key", "value")
+            }
             val client = createMockedSupabaseClient(configuration = configureClient) {
                 val data = it.body.toByteArray()
                 assertMethodIs(method, it.method)