diff --git a/firebase-functions/src/androidTest/backend/functions/index.js b/firebase-functions/src/androidTest/backend/functions/index.js index db1b9ab13e6..a51dbabdd4a 100644 --- a/firebase-functions/src/androidTest/backend/functions/index.js +++ b/firebase-functions/src/androidTest/backend/functions/index.js @@ -133,6 +133,10 @@ exports.timeoutTest = functions.https.onRequest((request, response) => { setTimeout(() => response.send({data: true}), 500); }); +exports.headersTest = functions.https.onRequest((request, response) => { + response.status(200).send({data: request.headers}); +}); + const streamData = ['hello', 'world', 'this', 'is', 'cool']; /** 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..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 @@ -90,4 +90,28 @@ class CallTests { assertThat(actual).isNull() } + + @Test + fun testCustomHeaders() { + val functions = Firebase.functions(app) + + val options = HttpsCallableOptions.Builder() + .addHeader("Header1", "value1") + .addHeader("Header2", "value2") + .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("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 8839763c4a3..0db9f9d9024 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 @@ -229,7 +230,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(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 215722584ba..5a8016f0180 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 @@ -202,4 +202,29 @@ public class HttpsCallableReference { 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 { + options.addHeader(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 { + options.addHeaders(headers) + return this + } }