200 lines
6.0 KiB
Dart
200 lines
6.0 KiB
Dart
part of '../app/app.dart';
|
|
|
|
class ApiClient {
|
|
ApiClient({required this.baseUrl, this.token});
|
|
|
|
final String baseUrl;
|
|
final String? token;
|
|
|
|
Uri _uri(String path, [Map<String, String>? query]) {
|
|
final normalizedBase = normalizeBaseUrl(baseUrl);
|
|
final base = Uri.parse(normalizedBase);
|
|
return base.replace(
|
|
path: '${base.path.replaceAll(RegExp(r'/$'), '')}/$path',
|
|
queryParameters: query,
|
|
);
|
|
}
|
|
|
|
Map<String, String> get _headers => {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
if (token != null) 'Authorization': 'Bearer $token',
|
|
};
|
|
|
|
Future<Map<String, dynamic>> post(
|
|
String path,
|
|
Map<String, dynamic> body,
|
|
) async {
|
|
final uri = _uri(path);
|
|
final client = _clientFor(uri);
|
|
try {
|
|
final response = await client
|
|
.post(uri, headers: _headers, body: jsonEncode(body))
|
|
.timeout(const Duration(seconds: 15));
|
|
return _decode(response, uri);
|
|
} catch (error, stackTrace) {
|
|
throw _connectionException(uri, error, stackTrace);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> get(
|
|
String path, [
|
|
Map<String, String>? query,
|
|
]) async {
|
|
final uri = _uri(path, query);
|
|
final client = _clientFor(uri);
|
|
try {
|
|
final response = await client
|
|
.get(uri, headers: _headers)
|
|
.timeout(const Duration(seconds: 15));
|
|
return _decode(response, uri);
|
|
} catch (error, stackTrace) {
|
|
throw _connectionException(uri, error, stackTrace);
|
|
} finally {
|
|
client.close();
|
|
}
|
|
}
|
|
|
|
http.Client _clientFor(Uri uri) {
|
|
final client = HttpClient()
|
|
..badCertificateCallback = (certificate, host, port) {
|
|
debugPrint(
|
|
'MyLIS menerima sertifikat internal untuk $host:$port '
|
|
'issuer=${certificate.issuer}',
|
|
);
|
|
return true;
|
|
};
|
|
return IOClient(client);
|
|
}
|
|
|
|
Map<String, dynamic> _decode(http.Response response, Uri uri) {
|
|
final bodyPreview = response.body.length > 240
|
|
? '${response.body.substring(0, 240)}...'
|
|
: response.body;
|
|
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
Object? decoded;
|
|
if (_looksLikeJson(response)) {
|
|
try {
|
|
decoded = response.body.isEmpty
|
|
? <String, dynamic>{}
|
|
: jsonDecode(response.body);
|
|
} catch (_) {
|
|
decoded = null;
|
|
}
|
|
}
|
|
final message = decoded is Map ? decoded['message'] : null;
|
|
debugPrint(
|
|
'MyLIS API gagal: ${response.request?.method ?? '-'} $uri '
|
|
'HTTP ${response.statusCode}; body=$bodyPreview',
|
|
);
|
|
throw ApiException(
|
|
message?.toString() ??
|
|
'Server mengembalikan HTTP ${response.statusCode}. '
|
|
'URL: $uri. Kemungkinan request masuk ke virtual host/proxy yang salah. '
|
|
'Detail awal respons: $bodyPreview',
|
|
);
|
|
}
|
|
|
|
final Object decoded;
|
|
try {
|
|
decoded = response.body.isEmpty
|
|
? <String, dynamic>{}
|
|
: jsonDecode(response.body);
|
|
} catch (error, stackTrace) {
|
|
debugPrint(
|
|
'MyLIS API decode gagal: ${response.request?.method ?? '-'} $uri '
|
|
'HTTP ${response.statusCode}; error=$error',
|
|
);
|
|
debugPrintStack(stackTrace: stackTrace);
|
|
throw ApiException(
|
|
'Respons server tidak bisa dibaca. '
|
|
'URL: $uri. Status HTTP: ${response.statusCode}.',
|
|
);
|
|
}
|
|
|
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
final cleanDecoded = _decodeHtmlEntitiesIn(decoded);
|
|
return cleanDecoded is Map<String, dynamic>
|
|
? cleanDecoded
|
|
: {'data': cleanDecoded};
|
|
}
|
|
return {'data': _decodeHtmlEntitiesIn(decoded)};
|
|
}
|
|
|
|
bool _looksLikeJson(http.Response response) {
|
|
final contentType = response.headers['content-type']?.toLowerCase() ?? '';
|
|
final body = response.body.trimLeft();
|
|
return contentType.contains('json') ||
|
|
body.startsWith('{') ||
|
|
body.startsWith('[');
|
|
}
|
|
|
|
ApiException _connectionException(
|
|
Uri uri,
|
|
Object error,
|
|
StackTrace stackTrace,
|
|
) {
|
|
if (error is ApiException) {
|
|
return error;
|
|
}
|
|
|
|
final reason = _connectionReason(error);
|
|
debugPrint('MyLIS koneksi gagal: $uri; $reason; error=$error');
|
|
debugPrintStack(stackTrace: stackTrace);
|
|
return ApiException('Koneksi server gagal. URL: $uri. Penyebab: $reason');
|
|
}
|
|
|
|
String _connectionReason(Object error) {
|
|
if (error is TimeoutException) {
|
|
return 'timeout lebih dari 15 detik. Periksa jaringan, DNS lokal, atau server Laravel belum aktif.';
|
|
}
|
|
if (error is http.ClientException) {
|
|
final message = error.message.toLowerCase();
|
|
if (message.contains('certificate') ||
|
|
message.contains('handshake') ||
|
|
message.contains('tls')) {
|
|
return 'sertifikat HTTPS lokal belum dipercaya oleh device/simulator.';
|
|
}
|
|
if (message.contains('failed host lookup') ||
|
|
message.contains('nodename') ||
|
|
message.contains('name or service')) {
|
|
return 'host tidak ditemukan. Periksa URL atau DNS/hosts lokal.';
|
|
}
|
|
if (message.contains('connection refused')) {
|
|
return 'server menolak koneksi. Periksa apakah Laravel/web server aktif.';
|
|
}
|
|
if (message.contains('connection closed')) {
|
|
return 'koneksi ditutup oleh server.';
|
|
}
|
|
return 'client HTTP gagal: ${error.message}';
|
|
}
|
|
if (error is FormatException) {
|
|
return 'format URL atau respons tidak valid: ${error.message}';
|
|
}
|
|
return error.toString();
|
|
}
|
|
|
|
Object? _decodeHtmlEntitiesIn(Object? value) {
|
|
if (value is String) {
|
|
return decodeHtmlEntities(value);
|
|
}
|
|
if (value is List) {
|
|
return value.map(_decodeHtmlEntitiesIn).toList();
|
|
}
|
|
if (value is Map) {
|
|
return value.map(
|
|
(key, item) => MapEntry(key.toString(), _decodeHtmlEntitiesIn(item)),
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
|
|
class ApiException implements Exception {
|
|
ApiException(this.message);
|
|
final String message;
|
|
}
|