Skip to content

Commit 09698ed

Browse files
authored
feat: Support MultipartRequest in functions invoke (#977)
* feat: support MultipartRequest in functions invoke * fix: export MultipartFile * test: add tests for body encoding * fix: set correct headers
1 parent 22cd12d commit 09698ed

File tree

5 files changed

+153
-16
lines changed

5 files changed

+153
-16
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
library functions_client;
22

3-
export 'package:http/http.dart' show ByteStream;
3+
export 'package:http/http.dart' show ByteStream, MultipartFile;
44

55
export 'src/functions_client.dart';
66
export 'src/types.dart';

packages/functions_client/lib/src/constants.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import 'package:functions_client/src/version.dart';
22

33
class Constants {
44
static const defaultHeaders = {
5-
'Content-Type': 'application/json',
65
'X-Client-Info': 'functions-dart/$version',
76
};
87
}

packages/functions_client/lib/src/functions_client.dart

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import 'dart:convert';
2+
import 'dart:typed_data';
23

34
import 'package:functions_client/src/constants.dart';
45
import 'package:functions_client/src/types.dart';
56
import 'package:http/http.dart' as http;
7+
import 'package:http/http.dart' show MultipartRequest;
68
import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
79

810
class FunctionsClient {
@@ -38,11 +40,17 @@ class FunctionsClient {
3840

3941
/// Invokes a function
4042
///
41-
/// [functionName] - the name of the function to invoke
43+
/// [functionName] is the name of the function to invoke
4244
///
43-
/// [headers]: object representing the headers to send with the request
45+
/// [headers] to send with the request
46+
///
47+
/// [body] of the request when [files] is null and can be of type String
48+
/// or an Object that is encodable to JSON with `jsonEncode`.
49+
/// If [files] is not null, [body] represents the fields of the
50+
/// [MultipartRequest] and must be be of type `Map<String, String>`.
51+
///
52+
/// [files] to send in a `MultipartRequest`. [body] is used for the fields.
4453
///
45-
/// [body]: the body of the request
4654
///
4755
/// ```dart
4856
/// // Call a standard function
@@ -70,12 +78,11 @@ class FunctionsClient {
7078
Future<FunctionResponse> invoke(
7179
String functionName, {
7280
Map<String, String>? headers,
73-
Map<String, dynamic>? body,
81+
Object? body,
82+
Iterable<http.MultipartFile>? files,
7483
Map<String, dynamic>? queryParameters,
7584
HttpMethod method = HttpMethod.post,
7685
}) async {
77-
final bodyStr = body == null ? null : await _isolate.encode(body);
78-
7986
final uri = Uri.parse('$_url/$functionName')
8087
.replace(queryParameters: queryParameters);
8188

@@ -84,12 +91,44 @@ class FunctionsClient {
8491
if (headers != null) ...headers
8592
};
8693

87-
final request = http.Request(method.name, uri);
94+
if (body != null &&
95+
(headers == null || headers.containsKey("Content-Type") == false)) {
96+
finalHeaders['Content-Type'] = switch (body) {
97+
Uint8List() => 'application/octet-stream',
98+
String() => 'text/plain',
99+
_ => 'application/json',
100+
};
101+
}
102+
final http.BaseRequest request;
103+
if (files != null) {
104+
assert(
105+
body == null || body is Map<String, String>,
106+
'body must be of type Map',
107+
);
108+
final fields = body as Map<String, String>?;
109+
110+
request = http.MultipartRequest(method.name, uri)
111+
..fields.addAll(fields ?? {})
112+
..files.addAll(files);
113+
} else {
114+
final bodyRequest = http.Request(method.name, uri);
115+
116+
final String? bodyStr;
117+
if (body == null) {
118+
bodyStr = null;
119+
} else if (body is String) {
120+
bodyStr = body;
121+
} else {
122+
bodyStr = await _isolate.encode(body);
123+
}
124+
if (bodyStr != null) bodyRequest.body = bodyStr;
125+
request = bodyRequest;
126+
}
88127

89128
finalHeaders.forEach((key, value) {
90129
request.headers[key] = value;
91130
});
92-
if (bodyStr != null) request.body = bodyStr;
131+
93132
final response = await (_httpClient?.send(request) ?? request.send());
94133
final responseType = (response.headers['Content-Type'] ??
95134
response.headers['content-type'] ??

packages/functions_client/test/custom_http_client.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ class CustomHttpClient extends BaseClient {
1313
Future<StreamedResponse> send(BaseRequest request) async {
1414
// Add request to receivedRequests list.
1515
receivedRequests = receivedRequests..add(request);
16+
request.finalize();
1617

17-
if (request.url.path.endsWith("function")) {
18+
if (request.url.path.endsWith("error-function")) {
1819
//Return custom status code to check for usage of this client.
1920
return StreamedResponse(
2021
Stream.value(utf8.encode(jsonEncode({"key": "Hello World"}))),
@@ -32,8 +33,22 @@ class CustomHttpClient extends BaseClient {
3233
"Content-Type": "text/event-stream",
3334
});
3435
} else {
36+
final Stream<List<int>> stream;
37+
if (request is MultipartRequest) {
38+
stream = Stream.value(
39+
utf8.encode(jsonEncode([
40+
for (final file in request.files)
41+
{
42+
"name": file.field,
43+
"content": await file.finalize().bytesToString()
44+
}
45+
])),
46+
);
47+
} else {
48+
stream = Stream.value(utf8.encode(jsonEncode({"key": "Hello World"})));
49+
}
3550
return StreamedResponse(
36-
Stream.value(utf8.encode(jsonEncode({"key": "Hello World"}))),
51+
stream,
3752
200,
3853
request: request,
3954
headers: {

packages/functions_client/test/functions_dart_test.dart

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22

33
import 'package:functions_client/src/functions_client.dart';
44
import 'package:functions_client/src/types.dart';
5+
import 'package:http/http.dart';
56
import 'package:test/test.dart';
67
import 'package:yet_another_json_isolate/yet_another_json_isolate.dart';
78

@@ -19,22 +20,24 @@ void main() {
1920
});
2021
test('function throws', () async {
2122
try {
22-
await functionsCustomHttpClient.invoke('function');
23+
await functionsCustomHttpClient.invoke('error-function');
2324
fail('should throw');
2425
} on FunctionException catch (e) {
2526
expect(e.status, 420);
2627
}
2728
});
2829

2930
test('function call', () async {
30-
final res = await functionsCustomHttpClient.invoke('function1');
31+
final res = await functionsCustomHttpClient.invoke('function');
32+
expect(
33+
customHttpClient.receivedRequests.last.headers["Content-Type"], null);
3134
expect(res.data, {'key': 'Hello World'});
3235
expect(res.status, 200);
3336
});
3437

3538
test('function call with query parameters', () async {
3639
final res = await functionsCustomHttpClient
37-
.invoke('function1', queryParameters: {'key': 'value'});
40+
.invoke('function', queryParameters: {'key': 'value'});
3841

3942
final request = customHttpClient.receivedRequests.last;
4043

@@ -43,6 +46,27 @@ void main() {
4346
expect(res.status, 200);
4447
});
4548

49+
test('function call with files', () async {
50+
final fileName = "file.txt";
51+
final fileContent = "Hello World";
52+
final res = await functionsCustomHttpClient.invoke(
53+
'function',
54+
queryParameters: {'key': 'value'},
55+
files: [
56+
MultipartFile.fromString(fileName, fileContent),
57+
],
58+
);
59+
60+
final request = customHttpClient.receivedRequests.last;
61+
62+
expect(request.url.queryParameters, {'key': 'value'});
63+
expect(request.headers['Content-Type'], contains('multipart/form-data'));
64+
expect(res.data, [
65+
{'name': fileName, 'content': fileContent}
66+
]);
67+
expect(res.status, 200);
68+
});
69+
4670
test('dispose isolate', () async {
4771
await functionsCustomHttpClient.dispose();
4872
expect(functionsCustomHttpClient.invoke('function'), throwsStateError);
@@ -57,7 +81,7 @@ void main() {
5781
);
5882

5983
await client.dispose();
60-
final res = await client.invoke('function1');
84+
final res = await client.invoke('function');
6185
expect(res.data, {'key': 'Hello World'});
6286
});
6387

@@ -69,5 +93,65 @@ void main() {
6993
['a', 'b', 'c'],
7094
));
7195
});
96+
97+
group('body encoding', () {
98+
test('integer properly encoded', () async {
99+
await functionsCustomHttpClient.invoke('function', body: 42);
100+
101+
final req = customHttpClient.receivedRequests.last;
102+
expect(req, isA<Request>());
103+
104+
req as Request;
105+
expect(req.body, '42');
106+
expect(req.headers["Content-Type"], contains("application/json"));
107+
});
108+
109+
test('double is properly encoded', () async {
110+
await functionsCustomHttpClient.invoke('function', body: 42.9);
111+
112+
final req = customHttpClient.receivedRequests.last;
113+
expect(req, isA<Request>());
114+
115+
req as Request;
116+
expect(req.body, '42.9');
117+
expect(req.headers["Content-Type"], contains("application/json"));
118+
});
119+
120+
test('string is properly encoded', () async {
121+
await functionsCustomHttpClient.invoke('function', body: 'ExampleText');
122+
123+
final req = customHttpClient.receivedRequests.last;
124+
expect(req, isA<Request>());
125+
126+
req as Request;
127+
expect(req.body, 'ExampleText');
128+
expect(req.headers["Content-Type"], contains("text/plain"));
129+
});
130+
131+
test('list is properly encoded', () async {
132+
await functionsCustomHttpClient.invoke('function', body: [1, 2, 3]);
133+
134+
final req = customHttpClient.receivedRequests.last;
135+
expect(req, isA<Request>());
136+
137+
req as Request;
138+
expect(req.body, '[1,2,3]');
139+
expect(req.headers["Content-Type"], contains("application/json"));
140+
});
141+
142+
test('map is properly encoded', () async {
143+
await functionsCustomHttpClient.invoke(
144+
'function',
145+
body: {'thekey': 'thevalue'},
146+
);
147+
148+
final req = customHttpClient.receivedRequests.last;
149+
expect(req, isA<Request>());
150+
151+
req as Request;
152+
expect(req.body, '{"thekey":"thevalue"}');
153+
expect(req.headers["Content-Type"], contains("application/json"));
154+
});
155+
});
72156
});
73157
}

0 commit comments

Comments
 (0)