From 1b180294d4683814c4a2cbef2a3aab7e70e43424 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 30 Sep 2025 17:00:04 -0300 Subject: [PATCH 1/2] feat: add region as forceFunctionRegion query parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add region parameter to FunctionsClient constructor and invoke method - When region is specified and not 'any', adds both x-region header and forceFunctionRegion query parameter - Add FunctionsClientOptions to SupabaseClient for configuring default region - Add comprehensive tests for region functionality - Maintain backward compatibility with existing code This feature ports the functionality from functions-js PR #100: https://github.com/supabase/functions-js/pull/100 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../lib/src/functions_client.dart | 26 ++++++- .../test/functions_dart_test.dart | 74 +++++++++++++++++++ .../supabase/lib/src/supabase_client.dart | 4 + .../lib/src/supabase_client_options.dart | 6 ++ 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/packages/functions_client/lib/src/functions_client.dart b/packages/functions_client/lib/src/functions_client.dart index 84b446743..7b7cb133b 100644 --- a/packages/functions_client/lib/src/functions_client.dart +++ b/packages/functions_client/lib/src/functions_client.dart @@ -15,6 +15,7 @@ class FunctionsClient { final http.Client? _httpClient; final YAJsonIsolate _isolate; final bool _hasCustomIsolate; + final String? _region; final _log = Logger("supabase.functions"); /// In case you don't provide your own isolate, call [dispose] when you're done @@ -23,11 +24,13 @@ class FunctionsClient { Map headers, { http.Client? httpClient, YAJsonIsolate? isolate, + String? region, }) : _url = url, _headers = {...Constants.defaultHeaders, ...headers}, _isolate = isolate ?? (YAJsonIsolate()..initialize()), _hasCustomIsolate = isolate != null, - _httpClient = httpClient { + _httpClient = httpClient, + _region = region { _log.config("Initialize FunctionsClient v$version with url: $url"); _log.finest("Initialize with headers: $headers"); } @@ -57,6 +60,8 @@ class FunctionsClient { /// /// [files] to send in a `MultipartRequest`. [body] is used for the fields. /// + /// [region] optionally specify the region to invoke the function in. + /// When specified, adds both `x-region` header and `forceFunctionRegion` query parameter. /// /// ```dart /// // Call a standard function @@ -88,13 +93,26 @@ class FunctionsClient { Iterable? files, Map? queryParameters, HttpMethod method = HttpMethod.post, + String? region, }) async { - final uri = Uri.parse('$_url/$functionName') - .replace(queryParameters: queryParameters); + final effectiveRegion = region ?? _region; + + // Merge query parameters with forceFunctionRegion if region is specified + final effectiveQueryParams = { + if (queryParameters != null) ...queryParameters, + if (effectiveRegion != null && effectiveRegion != 'any') + 'forceFunctionRegion': effectiveRegion, + }; + + final uri = Uri.parse('$_url/$functionName').replace( + queryParameters: + effectiveQueryParams.isNotEmpty ? effectiveQueryParams : null); final finalHeaders = { ..._headers, - if (headers != null) ...headers + if (headers != null) ...headers, + if (effectiveRegion != null && effectiveRegion != 'any') + 'x-region': effectiveRegion, }; if (body != null && diff --git a/packages/functions_client/test/functions_dart_test.dart b/packages/functions_client/test/functions_dart_test.dart index 48074f2cd..d212d1f9d 100644 --- a/packages/functions_client/test/functions_dart_test.dart +++ b/packages/functions_client/test/functions_dart_test.dart @@ -260,6 +260,80 @@ void main() { }); }); + group('Region support', () { + test( + 'region parameter adds x-region header and forceFunctionRegion query param', + () async { + await functionsCustomHttpClient.invoke( + 'function', + region: 'us-west-1', + ); + + final req = customHttpClient.receivedRequests.last; + expect(req.headers['x-region'], 'us-west-1'); + expect(req.url.queryParameters['forceFunctionRegion'], 'us-west-1'); + }); + + test('region "any" does not add header or query param', () async { + await functionsCustomHttpClient.invoke( + 'function', + region: 'any', + ); + + final req = customHttpClient.receivedRequests.last; + expect(req.headers.containsKey('x-region'), isFalse); + expect(req.url.queryParameters.containsKey('forceFunctionRegion'), + isFalse); + }); + + test('client region is used when invoke region is not specified', + () async { + final client = FunctionsClient( + "", + {}, + httpClient: customHttpClient, + region: 'eu-west-1', + ); + + await client.invoke('function'); + + final req = customHttpClient.receivedRequests.last; + expect(req.headers['x-region'], 'eu-west-1'); + expect(req.url.queryParameters['forceFunctionRegion'], 'eu-west-1'); + }); + + test('invoke region overrides client region', () async { + final client = FunctionsClient( + "", + {}, + httpClient: customHttpClient, + region: 'eu-west-1', + ); + + await client.invoke('function', region: 'us-east-1'); + + final req = customHttpClient.receivedRequests.last; + expect(req.headers['x-region'], 'us-east-1'); + expect(req.url.queryParameters['forceFunctionRegion'], 'us-east-1'); + }); + + test('region works with other query parameters', () async { + await functionsCustomHttpClient.invoke( + 'function', + region: 'ap-south-1', + queryParameters: {'key': 'value', 'foo': 'bar'}, + ); + + final req = customHttpClient.receivedRequests.last; + expect(req.headers['x-region'], 'ap-south-1'); + expect(req.url.queryParameters, { + 'key': 'value', + 'foo': 'bar', + 'forceFunctionRegion': 'ap-south-1', + }); + }); + }); + group('Constructor variations', () { test('constructor with all parameters', () { final isolate = YAJsonIsolate(); diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 382569f87..2bd00aac5 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -42,6 +42,7 @@ import 'counter.dart'; class SupabaseClient { final String _supabaseKey; final PostgrestClientOptions _postgrestOptions; + final FunctionsClientOptions _functionsOptions; final String _restUrl; final String _realtimeUrl; @@ -119,12 +120,14 @@ class SupabaseClient { PostgrestClientOptions postgrestOptions = const PostgrestClientOptions(), AuthClientOptions authOptions = const AuthClientOptions(), StorageClientOptions storageOptions = const StorageClientOptions(), + FunctionsClientOptions functionsOptions = const FunctionsClientOptions(), RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(), this.accessToken, Map? headers, Client? httpClient, YAJsonIsolate? isolate, }) : _supabaseKey = supabaseKey, + _functionsOptions = functionsOptions, _restUrl = '$supabaseUrl/rest/v1', _realtimeUrl = '$supabaseUrl/realtime/v1'.replaceAll('http', 'ws'), _authUrl = '$supabaseUrl/auth/v1', @@ -309,6 +312,7 @@ class SupabaseClient { {...headers}, httpClient: _authHttpClient, isolate: _isolate, + region: _functionsOptions.region, ); } diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart index 3587e6184..0c0c404fb 100644 --- a/packages/supabase/lib/src/supabase_client_options.dart +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -23,3 +23,9 @@ class StorageClientOptions { const StorageClientOptions({this.retryAttempts = 0}); } + +class FunctionsClientOptions { + final String? region; + + const FunctionsClientOptions({this.region}); +} From dd4710d40277cc30c416cd357c94abf6d22d8c4f Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 30 Sep 2025 19:02:20 -0300 Subject: [PATCH 2/2] add region to log --- packages/functions_client/lib/src/functions_client.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/functions_client/lib/src/functions_client.dart b/packages/functions_client/lib/src/functions_client.dart index 7b7cb133b..37e0e93c9 100644 --- a/packages/functions_client/lib/src/functions_client.dart +++ b/packages/functions_client/lib/src/functions_client.dart @@ -31,7 +31,8 @@ class FunctionsClient { _hasCustomIsolate = isolate != null, _httpClient = httpClient, _region = region { - _log.config("Initialize FunctionsClient v$version with url: $url"); + _log.config( + "Initialize FunctionsClient v$version with url '$url' and region '$region'"); _log.finest("Initialize with headers: $headers"); }