mirror of
https://github.com/inventree/inventree-app.git
synced 2025-04-28 13:36:50 +00:00
Improved GET requests
- Uses custom HttpClient - Callback for HTTPS certificate errors - Major code refactor = cleaner code! - All response validation is performed in the API now!
This commit is contained in:
parent
d6e9746d8c
commit
71340da068
226
lib/api.dart
226
lib/api.dart
@ -165,44 +165,19 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
print("Connecting to ${apiUrl} -> ${username}:${password}");
|
print("Connecting to ${apiUrl} -> ${username}:${password}");
|
||||||
|
|
||||||
var response = await get("").timeout(Duration(seconds: 10)).catchError((error) {
|
// Request the /api/ endpoint - response is a json object
|
||||||
|
var response = await get("");
|
||||||
print("Error connecting to server: ${error.toString()}");
|
|
||||||
|
|
||||||
if (error is SocketException) {
|
|
||||||
showServerError(
|
|
||||||
I18N.of(context).connectionRefused,
|
|
||||||
error.toString());
|
|
||||||
return null;
|
|
||||||
} else if (error is TimeoutException) {
|
|
||||||
showTimeoutError(context);
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
// Unknown error type - re-throw
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Null response means something went horribly wrong!
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
// Null (or error) response: Show dialog and exit
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
// Any status code other than 200!
|
|
||||||
|
|
||||||
showStatusCodeError(response.statusCode);
|
print("Response from server: ${response}");
|
||||||
|
|
||||||
// TODO: Interpret the error codes and show custom message?
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = json.decode(response.body);
|
|
||||||
|
|
||||||
print("Response from server: $data");
|
|
||||||
|
|
||||||
// We expect certain response from the server
|
// We expect certain response from the server
|
||||||
if (!data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) {
|
if (!response.containsKey("server") || !response.containsKey("version") || !response.containsKey("instance")) {
|
||||||
|
|
||||||
showServerError(
|
showServerError(
|
||||||
"Missing Data",
|
"Missing Data",
|
||||||
@ -213,11 +188,11 @@ class InvenTreeAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Record server information
|
// Record server information
|
||||||
_version = data["version"];
|
_version = response["version"];
|
||||||
instance = data['instance'] ?? '';
|
instance = response['instance'] ?? '';
|
||||||
|
|
||||||
// Default API version is 1 if not provided
|
// Default API version is 1 if not provided
|
||||||
_apiVersion = data['apiVersion'] as int ?? 1;
|
_apiVersion = response['apiVersion'] as int ?? 1;
|
||||||
|
|
||||||
if (_apiVersion < _minApiVersion) {
|
if (_apiVersion < _minApiVersion) {
|
||||||
|
|
||||||
@ -245,14 +220,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
print("Requesting token from server");
|
print("Requesting token from server");
|
||||||
|
|
||||||
response = await get(_URL_GET_TOKEN).timeout(Duration(seconds: 10)).catchError((error) {
|
response = await get(_URL_GET_TOKEN);
|
||||||
|
|
||||||
print("Error requesting token:");
|
|
||||||
print(error);
|
|
||||||
|
|
||||||
response = null;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
showServerError(
|
showServerError(
|
||||||
@ -263,31 +231,25 @@ class InvenTreeAPI {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (!response.containsKey("token")) {
|
||||||
showStatusCodeError(response.statusCode);
|
showServerError(
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
var data = json.decode(response.body);
|
|
||||||
|
|
||||||
if (!data.containsKey("token")) {
|
|
||||||
showServerError(
|
|
||||||
I18N.of(OneContext().context).tokenMissing,
|
I18N.of(OneContext().context).tokenMissing,
|
||||||
"Access token missing from response"
|
"Access token missing from response"
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the received token
|
// Return the received token
|
||||||
_token = data["token"];
|
_token = response["token"];
|
||||||
print("Received token - $_token");
|
print("Received token - $_token");
|
||||||
|
|
||||||
// Request user role information
|
// Request user role information
|
||||||
await getUserRoles();
|
await getUserRoles();
|
||||||
|
|
||||||
|
// Ok, probably pretty good...
|
||||||
|
return true;
|
||||||
|
|
||||||
// Ok, probably pretty good...
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool disconnectFromServer() {
|
bool disconnectFromServer() {
|
||||||
@ -348,32 +310,19 @@ class InvenTreeAPI {
|
|||||||
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
|
// Note: 2021-02-27 this "roles" feature for the API was just introduced.
|
||||||
// Any 'older' version of the server allows any API method for any logged in user!
|
// Any 'older' version of the server allows any API method for any logged in user!
|
||||||
// We will return immediately, but request the user roles in the background
|
// We will return immediately, but request the user roles in the background
|
||||||
await get(_URL_GET_ROLES).timeout(
|
|
||||||
Duration(seconds: 10)).catchError((error) {
|
|
||||||
print("Error requesting roles:");
|
|
||||||
print(error);
|
|
||||||
}).then((response) {
|
|
||||||
|
|
||||||
print("Response status: ${response.statusCode}");
|
var response = await get(_URL_GET_ROLES);
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
// Null response from server
|
||||||
|
if (response == null) {
|
||||||
|
print("null response requesting user roles");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert response to JSON representation
|
if (response.containsKey('roles')) {
|
||||||
|
// Save a local copy of the user roles
|
||||||
try {
|
roles = response['roles'];
|
||||||
var data = json.decode(response.body);
|
}
|
||||||
|
|
||||||
if (data.containsKey('roles')) {
|
|
||||||
// Save a local copy of the user roles
|
|
||||||
roles = data['roles'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
on FormatException {
|
|
||||||
// Old server has re-directed away from the API
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool checkPermission(String role, String permission) {
|
bool checkPermission(String role, String permission) {
|
||||||
@ -454,11 +403,42 @@ class InvenTreeAPI {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform a GET request
|
HttpClient _client(bool allowBadCert) {
|
||||||
Future<http.Response> get(String url, {Map<String, String> params}) async {
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
|
||||||
|
client.badCertificateCallback = ((X509Certificate cert, String host, int port) {
|
||||||
|
// TODO - Introspection of actual certificate?
|
||||||
|
|
||||||
|
if (allowBadCert) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
showServerError(
|
||||||
|
I18N.of(OneContext().context).serverCertificateError,
|
||||||
|
"Server HTTPS certificate invalid"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowBadCert;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set the connection timeout
|
||||||
|
client.connectionTimeout = Duration(seconds: 30);
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a HTTP GET request
|
||||||
|
* Returns a json object (or null if did not complete)
|
||||||
|
*/
|
||||||
|
Future<dynamic> get(String url, {Map<String, String> params}) async {
|
||||||
var _url = makeApiUrl(url);
|
var _url = makeApiUrl(url);
|
||||||
var _headers = defaultHeaders();
|
var _headers = defaultHeaders();
|
||||||
|
|
||||||
|
print("GET: ${_url}");
|
||||||
|
|
||||||
// If query parameters are supplied, form a query string
|
// If query parameters are supplied, form a query string
|
||||||
if (params != null && params.isNotEmpty) {
|
if (params != null && params.isNotEmpty) {
|
||||||
String query = '?';
|
String query = '?';
|
||||||
@ -475,7 +455,80 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
print("GET: " + _url);
|
print("GET: " + _url);
|
||||||
|
|
||||||
return http.get(_url, headers: _headers);
|
var client = _client(true);
|
||||||
|
|
||||||
|
print("Created client");
|
||||||
|
|
||||||
|
HttpClientRequest request = await client.getUrl(Uri.parse(_url));
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
|
||||||
|
if (profile != null) {
|
||||||
|
request.headers.set(
|
||||||
|
HttpHeaders.authorizationHeader,
|
||||||
|
_authorizationHeader(profile.username, profile.password)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Created request: ${request.uri}");
|
||||||
|
|
||||||
|
HttpClientResponse response = await request.close()
|
||||||
|
.timeout(Duration(seconds: 30))
|
||||||
|
.catchError((error) {
|
||||||
|
print("GET request returned error");
|
||||||
|
print("URL: ${_url}");
|
||||||
|
print("Error: ${error.toString()}");
|
||||||
|
|
||||||
|
var ctx = OneContext().context;
|
||||||
|
|
||||||
|
if (error is SocketException) {
|
||||||
|
showServerError(
|
||||||
|
I18N.of(ctx).connectionRefused,
|
||||||
|
error.toString()
|
||||||
|
);
|
||||||
|
} else if (error is TimeoutException) {
|
||||||
|
showTimeoutError(ctx);
|
||||||
|
} else {
|
||||||
|
showServerError(
|
||||||
|
I18N.of(ctx).serverError,
|
||||||
|
error.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// A null response means something has gone wrong...
|
||||||
|
if (response == null) {
|
||||||
|
print("null response from GET ${_url}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the status code of the response
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
showStatusCodeError(response.statusCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the body of the response to a JSON object
|
||||||
|
String body = await response.transform(utf8.decoder).join();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var data = json.decode(body);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} on FormatException {
|
||||||
|
|
||||||
|
print("JSON format exception!");
|
||||||
|
print("${body}");
|
||||||
|
|
||||||
|
showServerError(
|
||||||
|
"Format Exception",
|
||||||
|
"JSON data format exception:\n${body}"
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> defaultHeaders() {
|
Map<String, String> defaultHeaders() {
|
||||||
@ -496,6 +549,7 @@ class InvenTreeAPI {
|
|||||||
|
|
||||||
String _authorizationHeader(String username, String password) {
|
String _authorizationHeader(String username, String password) {
|
||||||
if (_token.isNotEmpty) {
|
if (_token.isNotEmpty) {
|
||||||
|
print("Using TOKEN: ${_token}");
|
||||||
return "Token $_token";
|
return "Token $_token";
|
||||||
} else {
|
} else {
|
||||||
return "Basic " + base64Encode(utf8.encode('${username}:${password}'));
|
return "Basic " + base64Encode(utf8.encode('${username}:${password}'));
|
||||||
|
@ -166,39 +166,13 @@ class InvenTreeModel {
|
|||||||
*/
|
*/
|
||||||
Future<bool> reload(BuildContext context) async {
|
Future<bool> reload(BuildContext context) async {
|
||||||
|
|
||||||
var response = await api.get(url, params: defaultGetFilters())
|
var response = await api.get(url, params: defaultGetFilters());
|
||||||
.timeout(Duration(seconds: 10))
|
|
||||||
.catchError((e) {
|
|
||||||
|
|
||||||
if (e is SocketException) {
|
|
||||||
showServerError(
|
|
||||||
I18N.of(context).connectionRefused,
|
|
||||||
e.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (e is TimeoutException) {
|
|
||||||
showTimeoutError(context);
|
|
||||||
} else {
|
|
||||||
// Re-throw the error
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
jsondata = response;
|
||||||
showStatusCodeError(response.statusCode);
|
|
||||||
print("Error retrieving data");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, dynamic> data = json.decode(response.body);
|
|
||||||
|
|
||||||
jsondata = data;
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -265,34 +239,13 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
print("GET: $addr ${params.toString()}");
|
print("GET: $addr ${params.toString()}");
|
||||||
|
|
||||||
var response = await api.get(addr, params: params)
|
var response = await api.get(addr, params: params);
|
||||||
.timeout(Duration(seconds: 10))
|
|
||||||
.catchError((e) {
|
|
||||||
|
|
||||||
if (e is SocketException) {
|
|
||||||
showServerError(I18N.of(context).connectionRefused, e.toString());
|
|
||||||
}
|
|
||||||
else if (e is TimeoutException) {
|
|
||||||
showTimeoutError(context);
|
|
||||||
} else {
|
|
||||||
// Re-throw the error
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
return createFromJson(response);
|
||||||
showStatusCodeError(response.statusCode);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = json.decode(response.body);
|
|
||||||
|
|
||||||
return createFromJson(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<InvenTreeModel> create(BuildContext context, Map<String, dynamic> data) async {
|
Future<InvenTreeModel> create(BuildContext context, Map<String, dynamic> data) async {
|
||||||
@ -353,41 +306,21 @@ class InvenTreeModel {
|
|||||||
params["limit"] = "${limit}";
|
params["limit"] = "${limit}";
|
||||||
params["offset"] = "${offset}";
|
params["offset"] = "${offset}";
|
||||||
|
|
||||||
var response = await api.get(URL, params: params)
|
var response = await api.get(URL, params: params);
|
||||||
.timeout(Duration(seconds: 10))
|
|
||||||
.catchError((error) {
|
|
||||||
if (error is SocketException) {
|
|
||||||
showServerError(
|
|
||||||
I18N
|
|
||||||
.of(OneContext().context)
|
|
||||||
.connectionRefused,
|
|
||||||
error.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == null) {
|
if (response == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
|
||||||
showStatusCodeError(response.statusCode);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct the response
|
// Construct the response
|
||||||
InvenTreePageResponse page = new InvenTreePageResponse();
|
InvenTreePageResponse page = new InvenTreePageResponse();
|
||||||
|
|
||||||
final data = json.decode(response.body);
|
if (response.containsKey("count") && response.containsKey("results")) {
|
||||||
|
page.count = response["count"] as int;
|
||||||
if (data.containsKey("count") && data.containsKey("results")) {
|
|
||||||
page.count = data["count"] as int;
|
|
||||||
|
|
||||||
page.results = [];
|
page.results = [];
|
||||||
|
|
||||||
for (var result in data["results"]) {
|
for (var result in response["results"]) {
|
||||||
page.addResult(createFromJson(result));
|
page.addResult(createFromJson(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -417,50 +350,20 @@ class InvenTreeModel {
|
|||||||
|
|
||||||
print("LIST: $URL ${params.toString()}");
|
print("LIST: $URL ${params.toString()}");
|
||||||
|
|
||||||
// TODO - Add "timeout"
|
var response = await api.get(URL, params: params);
|
||||||
// TODO - Add error catching
|
|
||||||
|
|
||||||
var response = await api.get(URL, params:params)
|
|
||||||
.timeout(Duration(seconds: 10))
|
|
||||||
.catchError((e) {
|
|
||||||
|
|
||||||
if (e is SocketException) {
|
|
||||||
showServerError(
|
|
||||||
I18N.of(context).connectionRefused,
|
|
||||||
e.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (e is TimeoutException) {
|
|
||||||
showTimeoutError(context);
|
|
||||||
} else {
|
|
||||||
// Re-throw the error
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A list of "InvenTreeModel" items
|
// A list of "InvenTreeModel" items
|
||||||
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
List<InvenTreeModel> results = new List<InvenTreeModel>();
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response == null) {
|
||||||
showStatusCodeError(response.statusCode);
|
|
||||||
|
|
||||||
// Return empty list
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = json.decode(response.body);
|
|
||||||
|
|
||||||
// TODO - handle possible error cases:
|
// TODO - handle possible error cases:
|
||||||
// - No data receieved
|
// - No data receieved
|
||||||
// - Data is not a list of maps
|
// - Data is not a list of maps
|
||||||
|
|
||||||
for (var d in data) {
|
for (var d in response) {
|
||||||
|
|
||||||
// Create a new object (of the current class type
|
// Create a new object (of the current class type
|
||||||
InvenTreeModel obj = createFromJson(d);
|
InvenTreeModel obj = createFromJson(d);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user