Skip to content

Commit e6c9420

Browse files
authored
fix(functions_client): Handle binary data request properly and improve test coverage (#1184)
* fix: Handle binary data request properly * format document
1 parent 88ed5d8 commit e6c9420

File tree

3 files changed

+260
-9
lines changed

3 files changed

+260
-9
lines changed

packages/functions_client/lib/src/functions_client.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,16 @@ class FunctionsClient {
119119
} else {
120120
final bodyRequest = http.Request(method.name, uri);
121121

122-
final String? bodyStr;
123122
if (body == null) {
124-
bodyStr = null;
123+
// No body to set
125124
} else if (body is String) {
126-
bodyStr = body;
125+
bodyRequest.body = body;
126+
} else if (body is Uint8List) {
127+
bodyRequest.bodyBytes = body;
127128
} else {
128-
bodyStr = await _isolate.encode(body);
129+
final bodyStr = await _isolate.encode(body);
130+
bodyRequest.body = bodyStr;
129131
}
130-
if (bodyStr != null) bodyRequest.body = bodyStr;
131132
request = bodyRequest;
132133
}
133134

packages/functions_client/test/custom_http_client.dart

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ class CustomHttpClient extends BaseClient {
2424
headers: {
2525
"Content-Type": "application/json",
2626
},
27+
reasonPhrase: "Enhance Your Calm",
2728
);
2829
} else if (request.url.path.endsWith('sse')) {
2930
return StreamedResponse(
@@ -32,8 +33,37 @@ class CustomHttpClient extends BaseClient {
3233
headers: {
3334
"Content-Type": "text/event-stream",
3435
});
36+
} else if (request.url.path.endsWith('binary')) {
37+
return StreamedResponse(
38+
Stream.value([1, 2, 3, 4, 5]),
39+
200,
40+
request: request,
41+
headers: {
42+
"Content-Type": "application/octet-stream",
43+
},
44+
);
45+
} else if (request.url.path.endsWith('text')) {
46+
return StreamedResponse(
47+
Stream.value(utf8.encode('Hello World')),
48+
200,
49+
request: request,
50+
headers: {
51+
"Content-Type": "text/plain",
52+
},
53+
);
54+
} else if (request.url.path.endsWith('empty-json')) {
55+
return StreamedResponse(
56+
Stream.value([]),
57+
200,
58+
request: request,
59+
headers: {
60+
"Content-Type": "application/json",
61+
},
62+
);
3563
} else {
3664
final Stream<List<int>> stream;
65+
final Map<String, String> headers;
66+
3767
if (request is MultipartRequest) {
3868
stream = Stream.value(
3969
utf8.encode(jsonEncode([
@@ -44,16 +74,27 @@ class CustomHttpClient extends BaseClient {
4474
}
4575
])),
4676
);
77+
headers = {"Content-Type": "application/json"};
4778
} else {
48-
stream = Stream.value(utf8.encode(jsonEncode({"key": "Hello World"})));
79+
// Check if the request contains binary data (Uint8List)
80+
final isOctetStream =
81+
request.headers['Content-Type'] == 'application/octet-stream';
82+
if (isOctetStream) {
83+
// Return the original binary data
84+
final bodyBytes = (request as Request).bodyBytes;
85+
stream = Stream.value(bodyBytes);
86+
headers = {"Content-Type": "application/octet-stream"};
87+
} else {
88+
stream =
89+
Stream.value(utf8.encode(jsonEncode({"key": "Hello World"})));
90+
headers = {"Content-Type": "application/json"};
91+
}
4992
}
5093
return StreamedResponse(
5194
stream,
5295
200,
5396
request: request,
54-
headers: {
55-
"Content-Type": "application/json",
56-
},
97+
headers: headers,
5798
);
5899
}
59100
}

packages/functions_client/test/functions_dart_test.dart

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'dart:typed_data';
23

34
import 'package:functions_client/src/functions_client.dart';
45
import 'package:functions_client/src/types.dart';
@@ -152,6 +153,214 @@ void main() {
152153
expect(req.body, '{"thekey":"thevalue"}');
153154
expect(req.headers["Content-Type"], contains("application/json"));
154155
});
156+
157+
test('Uint8List is properly encoded as binary data', () async {
158+
final binaryData = Uint8List.fromList([1, 2, 3, 4, 5]);
159+
await functionsCustomHttpClient.invoke('function', body: binaryData);
160+
161+
final req = customHttpClient.receivedRequests.last;
162+
expect(req, isA<Request>());
163+
164+
req as Request;
165+
expect(req.bodyBytes, equals(binaryData));
166+
expect(req.headers["Content-Type"], equals("application/octet-stream"));
167+
});
168+
169+
test('null body sends no content-type', () async {
170+
await functionsCustomHttpClient.invoke('function');
171+
172+
final req = customHttpClient.receivedRequests.last;
173+
expect(req, isA<Request>());
174+
175+
req as Request;
176+
expect(req.body, '');
177+
expect(req.headers.containsKey("Content-Type"), isFalse);
178+
});
179+
});
180+
181+
group('HTTP methods', () {
182+
test('GET method', () async {
183+
await functionsCustomHttpClient.invoke(
184+
'function',
185+
method: HttpMethod.get,
186+
);
187+
188+
final req = customHttpClient.receivedRequests.last;
189+
expect(req.method, 'get');
190+
});
191+
192+
test('PUT method', () async {
193+
await functionsCustomHttpClient.invoke(
194+
'function',
195+
method: HttpMethod.put,
196+
);
197+
198+
final req = customHttpClient.receivedRequests.last;
199+
expect(req.method, 'put');
200+
});
201+
202+
test('DELETE method', () async {
203+
await functionsCustomHttpClient.invoke(
204+
'function',
205+
method: HttpMethod.delete,
206+
);
207+
208+
final req = customHttpClient.receivedRequests.last;
209+
expect(req.method, 'delete');
210+
});
211+
212+
test('PATCH method', () async {
213+
await functionsCustomHttpClient.invoke(
214+
'function',
215+
method: HttpMethod.patch,
216+
);
217+
218+
final req = customHttpClient.receivedRequests.last;
219+
expect(req.method, 'patch');
220+
});
221+
});
222+
223+
group('Headers', () {
224+
test('setAuth updates authorization header', () async {
225+
functionsCustomHttpClient.setAuth('new-token');
226+
227+
await functionsCustomHttpClient.invoke('function');
228+
229+
final req = customHttpClient.receivedRequests.last;
230+
expect(req.headers['Authorization'], 'Bearer new-token');
231+
});
232+
233+
test('headers getter returns current headers', () {
234+
functionsCustomHttpClient.setAuth('test-token');
235+
236+
final headers = functionsCustomHttpClient.headers;
237+
expect(headers['Authorization'], 'Bearer test-token');
238+
expect(headers, contains('X-Client-Info'));
239+
});
240+
241+
test('custom headers override defaults', () async {
242+
await functionsCustomHttpClient.invoke(
243+
'function',
244+
headers: {'Content-Type': 'custom/type'},
245+
);
246+
247+
final req = customHttpClient.receivedRequests.last;
248+
expect(req.headers['Content-Type'], 'custom/type');
249+
});
250+
251+
test('custom headers merge with defaults', () async {
252+
await functionsCustomHttpClient.invoke(
253+
'function',
254+
headers: {'X-Custom': 'value'},
255+
);
256+
257+
final req = customHttpClient.receivedRequests.last;
258+
expect(req.headers['X-Custom'], 'value');
259+
expect(req.headers, contains('X-Client-Info'));
260+
});
261+
});
262+
263+
group('Constructor variations', () {
264+
test('constructor with all parameters', () {
265+
final isolate = YAJsonIsolate();
266+
final httpClient = CustomHttpClient();
267+
final client = FunctionsClient(
268+
'https://example.com',
269+
{'X-Test': 'value'},
270+
httpClient: httpClient,
271+
isolate: isolate,
272+
);
273+
274+
expect(client.headers['X-Test'], 'value');
275+
expect(client.headers, contains('X-Client-Info'));
276+
});
277+
278+
test('constructor with minimal parameters', () {
279+
final client = FunctionsClient('https://example.com', {});
280+
281+
expect(client.headers, contains('X-Client-Info'));
282+
});
283+
});
284+
285+
group('Multipart requests', () {
286+
test('multipart with both files and fields', () async {
287+
await functionsCustomHttpClient.invoke(
288+
'function',
289+
body: {'field1': 'value1', 'field2': 'value2'},
290+
files: [
291+
MultipartFile.fromString('file1', 'content1'),
292+
MultipartFile.fromString('file2', 'content2'),
293+
],
294+
);
295+
296+
final req = customHttpClient.receivedRequests.last;
297+
expect(req.headers['Content-Type'], contains('multipart/form-data'));
298+
expect(req, isA<MultipartRequest>());
299+
});
300+
301+
test('multipart with only files', () async {
302+
await functionsCustomHttpClient.invoke(
303+
'function',
304+
files: [MultipartFile.fromString('file', 'content')],
305+
);
306+
307+
final req = customHttpClient.receivedRequests.last;
308+
expect(req.headers['Content-Type'], contains('multipart/form-data'));
309+
expect(req, isA<MultipartRequest>());
310+
});
311+
});
312+
313+
group('Response content types', () {
314+
test('handles application/octet-stream response', () async {
315+
final res = await functionsCustomHttpClient.invoke('binary');
316+
317+
expect(res.data, isA<Uint8List>());
318+
expect(res.data, equals(Uint8List.fromList([1, 2, 3, 4, 5])));
319+
expect(res.status, 200);
320+
});
321+
322+
test('handles text/plain response', () async {
323+
final res = await functionsCustomHttpClient.invoke('text');
324+
325+
expect(res.data, isA<String>());
326+
expect(res.data, 'Hello World');
327+
expect(res.status, 200);
328+
});
329+
330+
test('handles empty JSON response', () async {
331+
final res = await functionsCustomHttpClient.invoke('empty-json');
332+
333+
expect(res.data, '');
334+
expect(res.status, 200);
335+
});
336+
});
337+
338+
group('Error handling', () {
339+
test('FunctionException contains all error details', () async {
340+
try {
341+
await functionsCustomHttpClient.invoke('error-function');
342+
fail('should throw');
343+
} on FunctionException catch (e) {
344+
expect(e.status, 420);
345+
expect(e.details, isNotNull);
346+
expect(e.reasonPhrase, isNotNull);
347+
expect(e.toString(), contains('420'));
348+
}
349+
});
350+
});
351+
352+
group('Edge cases', () {
353+
test('multipart request with invalid body type throws assertion',
354+
() async {
355+
expect(
356+
() => functionsCustomHttpClient.invoke(
357+
'function',
358+
body: 42, // Invalid: should be Map<String, String> for multipart
359+
files: [MultipartFile.fromString('file', 'content')],
360+
),
361+
throwsA(isA<AssertionError>()),
362+
);
363+
});
155364
});
156365
});
157366
}

0 commit comments

Comments
 (0)