call<TInput, TOutput> method
Future<TOutput>
call<TInput, TOutput>(
- BloomRpcContract<
TInput, TOutput> contract, - TInput input, {
- Map<
String, dynamic> ? pathParams, - Map<
String, dynamic> ? queryParameters, - Map<
String, String> ? headers, - Duration? timeout,
- BloomRpcCancelToken? cancelToken,
Executes a typed contract with input, returning the decoded TOutput.
pathParams: Map of path parameter substitutions for tokens in the contract path template.queryParameters: Additional query parameters appended to the URL.headers: Custom headers merged into request headers for this call.timeout: Custom timeout duration overriding BloomRpcClient.timeout.cancelToken: Token used to abort the request while in-flight.
Throws:
- BloomRpcHttpException: On non-2xx HTTP responses from the server.
- BloomRpcTransportException: On network disconnects or socket failures.
- BloomRpcDecodeException: When the server payload does not match
TOutput. - BloomRpcCancelledException: If aborted via
cancelToken. - BloomRpcTimeoutException: If the call exceeds
timeout.
final user = await client.call(
getUserContract,
null,
pathParams: {'id': '42'},
);
Implementation
Future<TOutput> call<TInput, TOutput>(
BloomRpcContract<TInput, TOutput> contract,
TInput input, {
Map<String, dynamic>? pathParams,
Map<String, dynamic>? queryParameters,
Map<String, String>? headers,
Duration? timeout,
BloomRpcCancelToken? cancelToken,
}) async {
cancelToken?.throwIfCancelled(contract);
// 1. Resolve path template
final String resolvedPath;
try {
resolvedPath =
contract.resolvePath(pathParams: pathParams, input: input);
} catch (e) {
if (e is BloomRpcException) rethrow;
throw BloomRpcEncodeException(cause: e, contract: contract);
}
// 2. Prepare merged headers
final mergedHeaders = <String, String>{
...defaultHeaders,
if (headers != null) ...headers,
};
// 3. Prepare payload and query parameters based on HTTP method
final isGetStyle = contract.method == BloomHttpMethod.get ||
contract.method == BloomHttpMethod.head ||
contract.method == BloomHttpMethod.options;
final mergedQueryParams = <String, dynamic>{};
if (queryParameters != null) {
mergedQueryParams.addAll(queryParameters);
}
dynamic bodyPayload;
if (isGetStyle) {
if (input != null) {
dynamic encoded;
if (contract.encodeInput != null) {
try {
encoded = contract.encodeInput!(input);
} catch (e, st) {
throw BloomRpcEncodeException(
cause: e, contract: contract, stackTrace: st);
}
} else {
encoded = input;
}
if (encoded is Map) {
final pathKeys = contract.pathParameters.toSet();
for (final entry in encoded.entries) {
final keyStr = entry.key.toString();
if (!pathKeys.contains(keyStr)) {
mergedQueryParams[keyStr] = entry.value;
}
}
}
}
} else {
if (input != null) {
if (contract.encodeInput != null) {
try {
bodyPayload = contract.encodeInput!(input);
} catch (e, st) {
throw BloomRpcEncodeException(
cause: e, contract: contract, stackTrace: st);
}
} else {
bodyPayload = input;
}
}
}
final effectiveTimeout = timeout ?? this.timeout;
// 4. Dispatch request
Future<dynamic> requestFuture;
switch (contract.method) {
case BloomHttpMethod.get:
case BloomHttpMethod.head:
case BloomHttpMethod.options:
requestFuture = httpClient.get(
resolvedPath,
headers: mergedHeaders,
queryParameters:
mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
);
break;
case BloomHttpMethod.post:
requestFuture = httpClient.post(
resolvedPath,
body: bodyPayload,
headers: mergedHeaders,
queryParameters:
mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
);
break;
case BloomHttpMethod.put:
requestFuture = httpClient.put(
resolvedPath,
body: bodyPayload,
headers: mergedHeaders,
queryParameters:
mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
);
break;
case BloomHttpMethod.patch:
requestFuture = httpClient.patch(
resolvedPath,
body: bodyPayload,
headers: mergedHeaders,
queryParameters:
mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
);
break;
case BloomHttpMethod.delete:
requestFuture = httpClient.delete(
resolvedPath,
body: bodyPayload,
headers: mergedHeaders,
queryParameters:
mergedQueryParams.isNotEmpty ? mergedQueryParams : null,
);
break;
}
// 5. Wrap with cancellation token and timeout
final dynamic rawResponse;
try {
rawResponse = await _raceWithCancellation(
requestFuture.timeout(effectiveTimeout),
cancelToken,
contract,
);
} on TimeoutException {
throw BloomRpcTimeoutException(
timeout: effectiveTimeout,
contract: contract,
);
} on BloomRpcException {
rethrow;
} on http.ClientException catch (e, st) {
// Inspect message for HTTP status pattern "HTTP <statusCode>: <body>"
final httpMatch =
RegExp(r'^HTTP\s+(\d{3}):\s*(.*)$', dotAll: true).firstMatch(e.message);
if (httpMatch != null) {
final statusCode = int.parse(httpMatch.group(1)!);
final rawBody = httpMatch.group(2) ?? '';
dynamic decodedBody;
try {
decodedBody = jsonDecode(rawBody);
} catch (_) {
decodedBody = rawBody;
}
throw BloomRpcHttpException(
statusCode: statusCode,
responseBody: decodedBody,
contract: contract,
uri: e.uri,
);
}
throw BloomRpcTransportException(
cause: e,
contract: contract,
uri: e.uri,
stackTrace: st,
);
} catch (e, st) {
throw BloomRpcTransportException(
cause: e,
contract: contract,
stackTrace: st,
);
}
// 6. Decode output
try {
// A void/nullable output with no body decodes to null. `TOutput == void`
// is not valid Dart; `null is TOutput` is the correct way to ask whether
// the output type admits null.
if (rawResponse == null && null is TOutput) {
return null as TOutput;
}
final decode = contract.decodeOutput ?? _defaultDecode<TOutput>;
return decode(rawResponse);
} catch (e, st) {
throw BloomRpcDecodeException(
cause: e,
rawData: rawResponse,
contract: contract,
stackTrace: st,
);
}
}