From 2dc0e55c89695f2a4f98478cf5880bccb91d261e Mon Sep 17 00:00:00 2001 From: Sven Jacobs Date: Tue, 10 Dec 2024 09:08:45 +0100 Subject: [PATCH 1/3] Add custom headers to HttpsCallableReference --- .../androidTest/backend/functions/index.js | 4 ++ .../google/firebase/functions/CallTests.kt | 14 ++++++ .../firebase/functions/FirebaseFunctions.kt | 22 +++++++--- .../functions/HttpsCallableReference.kt | 43 ++++++++++++++++--- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js index fed5a371b89..aa6c557e6fc 100644 --- a/firebase-functions/src/androidTest/backend/functions/index.js +++ b/firebase-functions/src/androidTest/backend/functions/index.js @@ -122,3 +122,7 @@ exports.timeoutTest = functions.https.onRequest((request, response) => { // Wait for longer than 500ms. setTimeout(() => response.send({data: true}), 500); }); + +exports.headersTest = functions.https.onRequest((request, response) => { + response.status(200).send({data: request.headers}); +}); diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt index 66829a34a44..89f605fe0d5 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt @@ -90,4 +90,18 @@ class CallTests { assertThat(actual).isNull() } + + @Test + fun testCustomHeaders() { + val functions = Firebase.functions(app) + val function = functions.getHttpsCallable("headersTest") + .addHeader("Header1", "value1") + .addHeader("Header2", "value2") + .addHeader("Header1", "value3") + val actual = Tasks.await(function.call()).getData() as? Map<*, *> + + assertThat(actual).isNotNull() + assertThat(actual?.get("Header1")).isEqualTo("value3") + assertThat(actual?.get("Header2")).isEqualTo("value2") + } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 824670c4346..0de65c63a3b 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -38,6 +38,7 @@ import java.util.concurrent.Executor import javax.inject.Named import okhttp3.Call import okhttp3.Callback +import okhttp3.Headers import okhttp3.MediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -174,7 +175,8 @@ internal constructor( internal fun call( name: String, data: Any?, - options: HttpsCallOptions + options: HttpsCallOptions, + headers: Map, ): Task { return providerInstalled.task .continueWithTask(executor) { contextProvider.getContext(options.limitedUseAppCheckTokens) } @@ -184,7 +186,7 @@ internal constructor( } val context = task.result val url = getURL(name) - call(url, data, context, options) + call(url, data, context, options, headers) } } @@ -195,7 +197,12 @@ internal constructor( * @param data Parameters to pass to the function. Can be anything encodable as JSON. * @return A Task that will be completed when the request is complete. */ - internal fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { + internal fun call( + url: URL, + data: Any?, + options: HttpsCallOptions, + headers: Map, + ): Task { return providerInstalled.task .continueWithTask(executor) { contextProvider.getContext(options.limitedUseAppCheckTokens) } .continueWithTask(executor) { task: Task -> @@ -203,7 +210,7 @@ internal constructor( return@continueWithTask Tasks.forException(task.exception!!) } val context = task.result - call(url, data, context, options) + call(url, data, context, options, headers) } } @@ -219,7 +226,8 @@ internal constructor( url: URL, data: Any?, context: HttpsCallableContext?, - options: HttpsCallOptions + options: HttpsCallOptions, + headers: Map, ): Task { Preconditions.checkNotNull(url, "url cannot be null") val body: MutableMap = HashMap() @@ -228,7 +236,9 @@ internal constructor( val bodyJSON = JSONObject(body) val contentType = MediaType.parse("application/json") val requestBody = RequestBody.create(contentType, bodyJSON.toString()) - var request = Request.Builder().url(url).post(requestBody) + // Add custom headers first so that internal headers cannot be overwritten + val customHeaders = Headers.of(headers) + var request = Request.Builder().url(url).post(requestBody).headers(customHeaders) if (context!!.authToken != null) { request = request.header("Authorization", "Bearer " + context.authToken) } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index 88db9db4ee4..1f71533e31e 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -34,16 +34,33 @@ public class HttpsCallableReference { // Options for how to do the HTTPS call. @VisibleForTesting internal val options: HttpsCallOptions + private val headers: MutableMap + + /** Creates a new reference with the given options. */ + internal constructor( + functionsClient: FirebaseFunctions, + name: String?, + options: HttpsCallOptions, + headers: MutableMap, + ) { + this.functionsClient = functionsClient + this.name = name + url = null + this.options = options + this.headers = headers + } + /** Creates a new reference with the given options. */ internal constructor( functionsClient: FirebaseFunctions, name: String?, - options: HttpsCallOptions + options: HttpsCallOptions, ) { this.functionsClient = functionsClient this.name = name url = null this.options = options + this.headers = mutableMapOf() } /** Creates a new reference with the given options. */ @@ -52,6 +69,7 @@ public class HttpsCallableReference { name = null this.url = url this.options = options + this.headers = mutableMapOf() } /** @@ -97,9 +115,9 @@ public class HttpsCallableReference { */ public fun call(data: Any?): Task { return if (name != null) { - functionsClient.call(name, data, options) + functionsClient.call(name, data, options, headers) } else { - functionsClient.call(url!!, data, options) + functionsClient.call(url!!, data, options, headers) } } @@ -119,9 +137,9 @@ public class HttpsCallableReference { */ public fun call(): Task { return if (name != null) { - functionsClient.call(name, null, options) + functionsClient.call(name, null, options, headers) } else { - functionsClient.call(url!!, null, options) + functionsClient.call(url!!, null, options, headers) } } @@ -150,8 +168,21 @@ public class HttpsCallableReference { * @param units The units for the specified timeout. */ public fun withTimeout(timeout: Long, units: TimeUnit): HttpsCallableReference { - val other = HttpsCallableReference(functionsClient, name, options) + val other = HttpsCallableReference(functionsClient, name, options, headers) other.setTimeout(timeout, units) return other } + + /** + * Adds an HTTP header for calls from this instance of Functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param name Name of HTTP header + * @param value Value of HTTP header + */ + public fun addHeader(name: String, value: String): HttpsCallableReference { + headers[name] = value + return this + } } From f5f52fadce78ef59a3925da0fdc7838a06e0543c Mon Sep 17 00:00:00 2001 From: Sven Jacobs Date: Thu, 23 Jan 2025 18:01:32 +0100 Subject: [PATCH 2/3] Add addHeaders() function --- .../java/com/google/firebase/functions/CallTests.kt | 2 ++ .../firebase/functions/HttpsCallableReference.kt | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt index 89f605fe0d5..71aa3156ed5 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt @@ -98,10 +98,12 @@ class CallTests { .addHeader("Header1", "value1") .addHeader("Header2", "value2") .addHeader("Header1", "value3") + .addHeaders(mapOf("Header3" to "value4")) val actual = Tasks.await(function.call()).getData() as? Map<*, *> assertThat(actual).isNotNull() assertThat(actual?.get("Header1")).isEqualTo("value3") assertThat(actual?.get("Header2")).isEqualTo("value2") + assertThat(actual?.get("Header3")).isEqualTo("value4") } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index 1f71533e31e..fa07b0e04d0 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -185,4 +185,16 @@ public class HttpsCallableReference { headers[name] = value return this } + + /** + * Adds all HTTP headers of passed map for calls from this instance of Functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param headers Map of HTTP headers (name to value) + */ + public fun addHeaders(headers: Map): HttpsCallableReference { + this.headers.putAll(headers) + return this + } } From 6e1cbc33d390d8a791e04bc6e278d064d3613143 Mon Sep 17 00:00:00 2001 From: Sven Jacobs Date: Fri, 24 Jan 2025 18:29:05 +0100 Subject: [PATCH 3/3] Add custom headers to HttpsCallableOptions --- .../google/firebase/functions/CallTests.kt | 18 +++++++--- .../firebase/functions/FirebaseFunctions.kt | 19 ++++------ .../firebase/functions/HttpsCallOptions.kt | 28 +++++++++++++++ .../functions/HttpsCallableOptions.kt | 36 +++++++++++++++++-- .../functions/HttpsCallableReference.kt | 34 +++++------------- 5 files changed, 89 insertions(+), 46 deletions(-) diff --git a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt index 71aa3156ed5..1e55aedf6f5 100644 --- a/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt +++ b/firebase-functions/src/androidTest/java/com/google/firebase/functions/CallTests.kt @@ -94,16 +94,24 @@ class CallTests { @Test fun testCustomHeaders() { val functions = Firebase.functions(app) - val function = functions.getHttpsCallable("headersTest") + + val options = HttpsCallableOptions.Builder() .addHeader("Header1", "value1") .addHeader("Header2", "value2") - .addHeader("Header1", "value3") - .addHeaders(mapOf("Header3" to "value4")) + .build() + + val function = functions.getHttpsCallable("headersTest", options) + .addHeader("Header2", "value3") + .addHeader("Header3", "value4") + .addHeader("Header4", "value5") + .addHeaders(mapOf("Header4" to "value6")) + val actual = Tasks.await(function.call()).getData() as? Map<*, *> assertThat(actual).isNotNull() - assertThat(actual?.get("Header1")).isEqualTo("value3") - assertThat(actual?.get("Header2")).isEqualTo("value2") + assertThat(actual?.get("Header1")).isEqualTo("value1") + assertThat(actual?.get("Header2")).isEqualTo("value3") assertThat(actual?.get("Header3")).isEqualTo("value4") + assertThat(actual?.get("Header4")).isEqualTo("value6") } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt index 0de65c63a3b..1fc5dc94c22 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/FirebaseFunctions.kt @@ -175,8 +175,7 @@ internal constructor( internal fun call( name: String, data: Any?, - options: HttpsCallOptions, - headers: Map, + options: HttpsCallOptions ): Task { return providerInstalled.task .continueWithTask(executor) { contextProvider.getContext(options.limitedUseAppCheckTokens) } @@ -186,7 +185,7 @@ internal constructor( } val context = task.result val url = getURL(name) - call(url, data, context, options, headers) + call(url, data, context, options) } } @@ -197,12 +196,7 @@ internal constructor( * @param data Parameters to pass to the function. Can be anything encodable as JSON. * @return A Task that will be completed when the request is complete. */ - internal fun call( - url: URL, - data: Any?, - options: HttpsCallOptions, - headers: Map, - ): Task { + internal fun call(url: URL, data: Any?, options: HttpsCallOptions): Task { return providerInstalled.task .continueWithTask(executor) { contextProvider.getContext(options.limitedUseAppCheckTokens) } .continueWithTask(executor) { task: Task -> @@ -210,7 +204,7 @@ internal constructor( return@continueWithTask Tasks.forException(task.exception!!) } val context = task.result - call(url, data, context, options, headers) + call(url, data, context, options) } } @@ -226,8 +220,7 @@ internal constructor( url: URL, data: Any?, context: HttpsCallableContext?, - options: HttpsCallOptions, - headers: Map, + options: HttpsCallOptions ): Task { Preconditions.checkNotNull(url, "url cannot be null") val body: MutableMap = HashMap() @@ -237,7 +230,7 @@ internal constructor( val contentType = MediaType.parse("application/json") val requestBody = RequestBody.create(contentType, bodyJSON.toString()) // Add custom headers first so that internal headers cannot be overwritten - val customHeaders = Headers.of(headers) + val customHeaders = Headers.of(options.headers) var request = Request.Builder().url(url).post(requestBody).headers(customHeaders) if (context!!.authToken != null) { request = request.header("Authorization", "Bearer " + context.authToken) diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt index f6b0e3f07c3..956a47e84c4 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallOptions.kt @@ -22,14 +22,17 @@ internal class HttpsCallOptions { private var timeout = DEFAULT_TIMEOUT private var timeoutUnits = DEFAULT_TIMEOUT_UNITS @JvmField public val limitedUseAppCheckTokens: Boolean + @JvmField val headers: MutableMap /** Creates an (internal) HttpsCallOptions from the (external) [HttpsCallableOptions]. */ internal constructor(publicCallableOptions: HttpsCallableOptions) { limitedUseAppCheckTokens = publicCallableOptions.limitedUseAppCheckTokens + headers = publicCallableOptions.headers.toMutableMap() } internal constructor() { limitedUseAppCheckTokens = false + headers = mutableMapOf() } internal fun getLimitedUseAppCheckTokens(): Boolean { @@ -56,6 +59,31 @@ internal class HttpsCallOptions { return timeoutUnits.toMillis(timeout) } + /** + * Adds an HTTP header for calls from this instance of Functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param name Name of HTTP header + * @param value Value of HTTP header + */ + internal fun addHeader(name: String, value: String): HttpsCallOptions { + headers[name] = value + return this + } + + /** + * Adds all HTTP headers of passed map for calls from this instance of Functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param headers Map of HTTP headers (name to value) + */ + internal fun addHeaders(headers: Map): HttpsCallOptions { + this.headers.putAll(headers) + return this + } + /** Creates a new OkHttpClient with these options applied to it. */ internal fun apply(client: OkHttpClient): OkHttpClient { return client diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt index 63aa4547e64..4b1c87e887d 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableOptions.kt @@ -24,16 +24,23 @@ private constructor( * Returns the setting indicating if limited-use App Check tokens are enforced for this function. */ // If true, request a limited-use token from AppCheck. - @JvmField public val limitedUseAppCheckTokens: Boolean + @JvmField public val limitedUseAppCheckTokens: Boolean, + @JvmField internal val headers: Map, ) { public fun getLimitedUseAppCheckTokens(): Boolean { return limitedUseAppCheckTokens } + public fun getHeaders(): Map { + // Returning a defensive copy + return headers.toMap() + } + /** A builder for creating [com.google.firebase.functions.HttpsCallableOptions]. */ public class Builder { @JvmField public var limitedUseAppCheckTokens: Boolean = false + private val headers: MutableMap = mutableMapOf() /** Returns the setting indicating if limited-use App Check tokens are enforced. */ public fun getLimitedUseAppCheckTokens(): Boolean { @@ -49,9 +56,34 @@ private constructor( return this } + /** + * Adds an HTTP header for callable functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param name Name of HTTP header + * @param value Value of HTTP header + */ + public fun addHeader(name: String, value: String): Builder { + headers[name] = value + return this + } + + /** + * Adds all HTTP headers of passed map for callable functions. + * + * Note that an existing header with the same name will be overwritten. + * + * @param headers Map of HTTP headers (name to value) + */ + public fun addHeaders(headers: Map): Builder { + this.headers.putAll(headers) + return this + } + /** Builds a new [com.google.firebase.functions.HttpsCallableOptions]. */ public fun build(): HttpsCallableOptions { - return HttpsCallableOptions(limitedUseAppCheckTokens) + return HttpsCallableOptions(limitedUseAppCheckTokens, headers.toMap()) } } } diff --git a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt index fa07b0e04d0..7c40df6b9a7 100644 --- a/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt +++ b/firebase-functions/src/main/java/com/google/firebase/functions/HttpsCallableReference.kt @@ -34,33 +34,16 @@ public class HttpsCallableReference { // Options for how to do the HTTPS call. @VisibleForTesting internal val options: HttpsCallOptions - private val headers: MutableMap - - /** Creates a new reference with the given options. */ - internal constructor( - functionsClient: FirebaseFunctions, - name: String?, - options: HttpsCallOptions, - headers: MutableMap, - ) { - this.functionsClient = functionsClient - this.name = name - url = null - this.options = options - this.headers = headers - } - /** Creates a new reference with the given options. */ internal constructor( functionsClient: FirebaseFunctions, name: String?, - options: HttpsCallOptions, + options: HttpsCallOptions ) { this.functionsClient = functionsClient this.name = name url = null this.options = options - this.headers = mutableMapOf() } /** Creates a new reference with the given options. */ @@ -69,7 +52,6 @@ public class HttpsCallableReference { name = null this.url = url this.options = options - this.headers = mutableMapOf() } /** @@ -115,9 +97,9 @@ public class HttpsCallableReference { */ public fun call(data: Any?): Task { return if (name != null) { - functionsClient.call(name, data, options, headers) + functionsClient.call(name, data, options) } else { - functionsClient.call(url!!, data, options, headers) + functionsClient.call(url!!, data, options) } } @@ -137,9 +119,9 @@ public class HttpsCallableReference { */ public fun call(): Task { return if (name != null) { - functionsClient.call(name, null, options, headers) + functionsClient.call(name, null, options) } else { - functionsClient.call(url!!, null, options, headers) + functionsClient.call(url!!, null, options) } } @@ -168,7 +150,7 @@ public class HttpsCallableReference { * @param units The units for the specified timeout. */ public fun withTimeout(timeout: Long, units: TimeUnit): HttpsCallableReference { - val other = HttpsCallableReference(functionsClient, name, options, headers) + val other = HttpsCallableReference(functionsClient, name, options) other.setTimeout(timeout, units) return other } @@ -182,7 +164,7 @@ public class HttpsCallableReference { * @param value Value of HTTP header */ public fun addHeader(name: String, value: String): HttpsCallableReference { - headers[name] = value + options.addHeader(name, value) return this } @@ -194,7 +176,7 @@ public class HttpsCallableReference { * @param headers Map of HTTP headers (name to value) */ public fun addHeaders(headers: Map): HttpsCallableReference { - this.headers.putAll(headers) + options.addHeaders(headers) return this } }