2
0
mirror of https://github.com/inventree/inventree-app.git synced 2025-04-28 13:36:50 +00:00

Refactor API requests

- Return a non-nullable APIRequest object
- Contains status-code, url, method, etc
- Pass all API requests through a common function which does error handling
This commit is contained in:
Oliver 2021-07-16 16:39:33 +10:00
parent e637e2a9bc
commit 6ed0acce27
3 changed files with 214 additions and 376 deletions

View File

@ -17,6 +17,27 @@ import 'package:inventree/l10.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
/**
* Class representing an API response from the server
*/
class APIResponse {
APIResponse({this.url = "", this.method = "", this.statusCode = -1, this.data});
int statusCode = -1;
String url = "";
String method = "";
dynamic data;
// Request is "valid" if a statusCode was returned
bool isValid() => statusCode >= 0;
}
/** /**
* Custom FileService for caching network images * Custom FileService for caching network images
* Requires a custom badCertificateCallback, * Requires a custom badCertificateCallback,
@ -214,16 +235,12 @@ class InvenTreeAPI {
print("Connecting to ${apiUrl} -> username=${username}"); print("Connecting to ${apiUrl} -> username=${username}");
HttpClientResponse? response; APIResponse response;
dynamic data; response = await get("", expectedStatusCode: 200);
response = await getResponse(""); // Response was invalid for some reason
if (!response.isValid()) {
// Null response means something went horribly wrong!
// Most likely, the server cannot be contacted
if (response == null) {
// An error message has already been displayed!
return false; return false;
} }
@ -232,10 +249,8 @@ class InvenTreeAPI {
return false; return false;
} }
data = await responseToJson(response);
// We expect certain response from the server // We expect certain response from the server
if (data == null || !data.containsKey("server") || !data.containsKey("version") || !data.containsKey("instance")) { if (response.data == null || !response.data.containsKey("server") || !response.data.containsKey("version") || !response.data.containsKey("instance")) {
showServerError( showServerError(
L10().missingData, L10().missingData,
@ -246,11 +261,11 @@ class InvenTreeAPI {
} }
// Record server information // Record server information
_version = data["version"]; _version = response.data["version"];
instance = data['instance'] ?? ''; instance = response.data['instance'] ?? '';
// Default API version is 1 if not provided // Default API version is 1 if not provided
_apiVersion = (data['apiVersion'] ?? 1) as int; _apiVersion = (response.data['apiVersion'] ?? 1) as int;
if (_apiVersion < _minApiVersion) { if (_apiVersion < _minApiVersion) {
@ -280,10 +295,10 @@ class InvenTreeAPI {
print("Requesting token from server"); print("Requesting token from server");
response = await getResponse(_URL_GET_TOKEN); response = await get(_URL_GET_TOKEN);
// A "null" response means that the request was unsuccessful // Invalid response
if (response == null) { if (!response.isValid()) {
return false; return false;
} }
@ -305,9 +320,7 @@ class InvenTreeAPI {
return false; return false;
} }
data = await responseToJson(response); if (response.data == null || !response.data.containsKey("token")) {
if (data == null || !data.containsKey("token")) {
showServerError( showServerError(
L10().tokenMissing, L10().tokenMissing,
L10().tokenMissingFromResponse, L10().tokenMissingFromResponse,
@ -317,7 +330,7 @@ class InvenTreeAPI {
} }
// Return the received token // Return the received token
_token = data["token"]; _token = response.data["token"];
print("Received token - $_token"); print("Received token - $_token");
// Request user role information // Request user role information
@ -383,17 +396,15 @@ class InvenTreeAPI {
// 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
var response = await get(_URL_GET_ROLES); var response = await get(_URL_GET_ROLES, expectedStatusCode: 200);
// Null response from server if (!response.isValid() || response.statusCode != 200) {
if (response == null) {
print("null response requesting user roles");
return; return;
} }
if (response.containsKey('roles')) { if (response.data.containsKey('roles')) {
// Save a local copy of the user roles // Save a local copy of the user roles
roles = response['roles']; roles = response.data['roles'];
} }
} }
@ -420,107 +431,27 @@ class InvenTreeAPI {
// Perform a PATCH request // Perform a PATCH request
Future<dynamic> patch(String url, {Map<String, String> body = const {}, int expectedStatusCode=200}) async { Future<APIResponse> patch(String url, {Map<String, String> body = const {}, int expectedStatusCode=200}) async {
var _url = makeApiUrl(url);
var _body = Map<String, String>(); var _body = Map<String, String>();
// Copy across provided data // Copy across provided data
body.forEach((K, V) => _body[K] = V); body.forEach((K, V) => _body[K] = V);
print("PATCH: " + _url); HttpClientRequest? request = await apiRequest(url, "PATCH");
final uri = Uri.parse(_url); if (request == null) {
// Return an "invalid" APIResponse
// Check for invalid host return new APIResponse(
if (uri.host.isEmpty) { url: url,
showServerError(L10().invalidHost, L10().invalidHostDetails); method: 'PATCH',
return null;
}
var client = createClient(true);
HttpClientRequest? request;
try {
// Open a connection to the server
request = await client.patchUrl(uri).timeout(Duration(seconds: 10));
} on SocketException catch (error) {
showServerError(L10().connectionRefused, error.toString());
return null;
} on TimeoutException {
showTimeoutError();
return null;
} catch (error, stackTrace) {
showServerError(
L10().serverError,
error.toString()
); );
sentryReportError(error, stackTrace);
return null;
} }
var data = json.encode(_body); return completeRequest(
request,
// Set headers data: json.encode(_body),
request.headers.set(HttpHeaders.acceptHeader, 'application/json'); statusCode: expectedStatusCode
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json'); );
request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
request.headers.set(HttpHeaders.contentLengthHeader, data.length.toString());
request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
request.add(utf8.encode(data));
HttpClientResponse? response;
try {
response = await request.close().timeout(Duration(seconds: 10));
} on SocketException catch (error) {
showServerError(
L10().connectionRefused,
error.toString()
);
return null;
} on TimeoutException {
showTimeoutError();
return null;
} catch (error, stackTrace) {
showServerError(
L10().serverError,
error.toString()
);
sentryReportError(error, stackTrace);
return null;
}
var responseData = await responseToJson(response);
if (response.statusCode != expectedStatusCode) {
showStatusCodeError(response.statusCode);
print("PATCH to ${_url} returned status code ${response.statusCode}");
print("Data:");
print(responseData);
// Server error
if (response.statusCode >= 500) {
sentryReportMessage(
"Server error on PATCH request",
context: {
"url": _url,
"statusCode": "${response.statusCode}",
"response": responseData.toString(),
"request": body.toString(),
}
);
}
return null;
}
return responseData;
} }
/* /*
@ -571,155 +502,42 @@ class InvenTreeAPI {
* We send this with the currently selected "locale", * We send this with the currently selected "locale",
* so that (hopefully) the field messages are correctly translated * so that (hopefully) the field messages are correctly translated
*/ */
Future<dynamic> options(String url) async { Future<APIResponse> options(String url) async {
var _url = makeApiUrl(url); HttpClientRequest? request = await apiRequest(url, "OPTIONS");
var client = createClient(true); if (request == null) {
// Return an "invalid" APIResponse
final uri = Uri.parse(_url); return new APIResponse(
url: url,
if (uri.host.isEmpty) { method: 'OPTIONS'
showServerError(L10().invalidHost, L10().invalidHostDetails); );
return null;
} }
HttpClientRequest? request; return completeRequest(request);
HttpClientResponse? response;
try {
request = await client.openUrl("OPTIONS", uri).timeout(Duration(seconds: 10));
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
response = await request.close().timeout(Duration(seconds: 10));
} on SocketException catch (error) {
showServerError(L10().connectionRefused, error.toString());
return null;
} on TimeoutException {
showTimeoutError();
return null;
} catch (error, stackTrace) {
showServerError(L10().serverError, error.toString());
sentryReportError(error, stackTrace);
return null;
}
var responseData = await responseToJson(response);
return responseData;
} }
/** /**
* Perform a HTTP POST request * Perform a HTTP POST request
* Returns a json object (or null if unsuccessful) * Returns a json object (or null if unsuccessful)
*/ */
Future<dynamic> post(String url, {Map<String, dynamic> body = const {}, int expectedStatusCode=201}) async { Future<APIResponse> post(String url, {Map<String, dynamic> body = const {}, int expectedStatusCode=201}) async {
var _url = makeApiUrl(url); HttpClientRequest? request = await apiRequest(url, "POST");
print("POST: ${_url} -> ${body.toString()}"); if (request == null) {
// Return an "invalid" APIResponse
var client = createClient(true); return new APIResponse(
url: url,
final uri = Uri.parse(_url); method: 'POST'
);
if (uri.host.isEmpty) {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
} }
HttpClientRequest? request; return completeRequest(
request,
try { data: json.encode(body),
// Open a connection to the server statusCode: expectedStatusCode
request = await client.postUrl(uri).timeout(Duration(seconds: 10)); );
} on SocketException catch (error) {
showServerError(
L10().connectionRefused,
error.toString()
);
return null;
} on TimeoutException {
showTimeoutError();
return null;
} catch (error, stackTrace) {
showServerError(
L10().serverError,
error.toString()
);
sentryReportError(error, stackTrace);
return null;
}
var data = json.encode(body);
// Set headers
// Ref: https://stackoverflow.com/questions/59713003/body-not-sending-using-map-in-flutter
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
request.headers.set(HttpHeaders.contentLengthHeader, data.length.toString());
request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
// Add JSON data to the request
request.add(utf8.encode(data));
HttpClientResponse? response;
try {
response = await request.close().timeout(Duration(seconds: 10));
} on SocketException catch (error) {
showServerError(
L10().connectionRefused,
error.toString()
);
return null;
} on TimeoutException {
showTimeoutError();
return null;
} catch (error, stackTrace) {
showServerError(
L10().serverError,
error.toString()
);
sentryReportError(error, stackTrace);
return null;
}
var responseData = await responseToJson(response);
if (response.statusCode != expectedStatusCode) {
showStatusCodeError(response.statusCode);
print("POST to ${_url} returned status code ${response.statusCode}");
print("Data:");
print(responseData);
// Server error
if (response.statusCode >= 500) {
sentryReportMessage(
"Server error on POST request",
context: {
"url": _url,
"statusCode": "${response.statusCode}",
"response": responseData.toString(),
"request": body.toString(),
}
);
}
return null;
}
return responseData;
} }
HttpClient createClient(bool allowBadCert) { HttpClient createClient(bool allowBadCert) {
@ -749,20 +567,21 @@ class InvenTreeAPI {
} }
/** /**
* Perform a HTTP GET request, * Initiate a HTTP request to the server
* and return the Response object *
* (or null if the request fails) * @param url is the API endpoint
* @param method is the HTTP method e.g. 'POST' / 'PATCH' / 'GET' etc;
* @param params is the request parameters
*/ */
Future<HttpClientResponse?> getResponse(String url, {Map<String, String> params = const {}}) async { Future<HttpClientRequest?> apiRequest(String url, String method, {Map<String, String> urlParams = const {}}) async {
var _url = makeApiUrl(url); var _url = makeApiUrl(url);
print("GET: ${_url}"); // Add any required query parameters to the URL using ?key=value notation
if (urlParams.isNotEmpty) {
String query = "?";
// If query parameters are supplied, form a query string urlParams.forEach((k, v) => query += "${k}=${v}&");
if (params.isNotEmpty) {
String query = '?';
params.forEach((K, V) => query += K + '=' + V + '&');
_url += query; _url += query;
} }
@ -772,70 +591,106 @@ class InvenTreeAPI {
_url = _url.substring(0, _url.length - 1); _url = _url.substring(0, _url.length - 1);
} }
Uri? _uri = Uri.tryParse(_url);
print("apiRequest ${method} -> ${url}");
if (_uri == null) {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
}
if (_uri.host.isEmpty) {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
}
HttpClientRequest? _request;
var client = createClient(true); var client = createClient(true);
Uri? uri = Uri.tryParse(_url); // Attempt to open a connection to the server
if (uri == null) {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
}
// Check for invalid host
if (uri.host.isEmpty) {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
}
HttpClientRequest? request;
try { try {
// Open a connection _request = await client.openUrl(method, _uri).timeout(Duration(seconds: 10));
request = await client.getUrl(uri).timeout(Duration(seconds: 10));
} on TimeoutException { // Set headers
showTimeoutError(); _request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
return null; _request.headers.set(HttpHeaders.acceptHeader, 'application/json');
_request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
_request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
return _request;
} on SocketException catch (error) { } on SocketException catch (error) {
showServerError(L10().connectionRefused, error.toString()); showServerError(L10().connectionRefused, error.toString());
return null; return null;
} on FormatException {
showServerError(L10().invalidHost, L10().invalidHostDetails);
return null;
} catch (error, stackTrace) {
sentryReportError(error, stackTrace);
showServerError(L10().serverError, error.toString());
return null;
}
// Set connection headers
request.headers.set(HttpHeaders.acceptHeader, 'application/json');
request.headers.set(HttpHeaders.contentTypeHeader, 'application/json');
request.headers.set(HttpHeaders.acceptLanguageHeader, Intl.getCurrentLocale());
request.headers.set(HttpHeaders.authorizationHeader, _authorizationHeader());
try {
HttpClientResponse response = await request.close().timeout(Duration(seconds: 10));
return response;
} on TimeoutException { } on TimeoutException {
showTimeoutError(); showTimeoutError();
return null; return null;
} on SocketException catch (error) {
showServerError(L10().connectionRefused, error.toString());
return null;
} catch (error, stackTrace) { } catch (error, stackTrace) {
showServerError(L10().serverError, error.toString());
showServerError( sentryReportError(error, stackTrace);
L10().serverError,
error.toString()
);
sentryReportError(error, stackTrace);
return null; return null;
} }
} }
/**
* Complete an API request, and return an APIResponse object
*/
Future<APIResponse> completeRequest(HttpClientRequest request, {String? data, int? statusCode}) async {
if (data != null && data.isNotEmpty) {
request.headers.set(HttpHeaders.contentLengthHeader, data.length.toString());
request.add(utf8.encode(data));
}
APIResponse response = new APIResponse(
method: request.method,
url: request.uri.toString()
);
try {
HttpClientResponse? _response = await request.close().timeout(Duration(seconds: 10));
response.statusCode = _response.statusCode;
response.data = await responseToJson(_response);
// Expected status code not returned
if ((statusCode != null) && (statusCode != _response.statusCode)) {
showStatusCodeError(_response.statusCode);
}
// Report any server errors
if (_response.statusCode >= 500) {
sentryReportMessage(
"Server error",
context: {
"url": request.uri.toString(),
"method": request.method,
"statusCode": _response.statusCode.toString(),
"requestHeaders": request.headers.toString(),
"responseHeaders": _response.headers.toString(),
"responseData": response.data.toString(),
}
);
}
} on SocketException catch (error) {
showServerError(L10().connectionRefused, error.toString());
} on TimeoutException {
showTimeoutError();
} catch (error, stackTrace) {
showServerError(L10().serverError, error.toString());
sentryReportError(error, stackTrace);
}
return response;
}
/**
* Convert a HttpClientResponse response object to JSON
*/
dynamic responseToJson(HttpClientResponse response) async { dynamic responseToJson(HttpClientResponse response) async {
String body = await response.transform(utf8.decoder).join(); String body = await response.transform(utf8.decoder).join();
@ -871,55 +726,37 @@ class InvenTreeAPI {
* Perform a HTTP GET request * Perform a HTTP GET request
* Returns a json object (or null if did not complete) * Returns a json object (or null if did not complete)
*/ */
Future<dynamic> get(String url, {Map<String, String> params = const {}, int expectedStatusCode=200}) async { Future<APIResponse> get(String url, {Map<String, String> params = const {}, int expectedStatusCode=200}) async {
var response = await getResponse(url, params: params); HttpClientRequest? request = await apiRequest(
url,
"GET",
urlParams: params,
);
// A null response means something has gone wrong... if (request == null) {
if (response == null) { // Return an "invalid" APIResponse
print("null response from GET ${url}"); return new APIResponse(
return null; url: url,
method: 'GET',
);
} }
var responseData = await responseToJson(response); return completeRequest(request);
// Check the status code of the response
if (response.statusCode != expectedStatusCode) {
showStatusCodeError(response.statusCode);
// Server error
if (response.statusCode >= 500) {
sentryReportMessage(
"Server error on GET request",
context: {
"url": url,
"statusCode": "${response.statusCode}",
"response": responseData.toString(),
"params": params.toString(),
}
);
}
return null;
}
return responseData;
} }
// Return a list of request headers
Map<String, String> defaultHeaders() { Map<String, String> defaultHeaders() {
var headers = Map<String, String>(); var headers = Map<String, String>();
headers[HttpHeaders.authorizationHeader] = _authorizationHeader(); headers[HttpHeaders.authorizationHeader] = _authorizationHeader();
headers[HttpHeaders.acceptHeader] = 'application/json';
headers[HttpHeaders.contentTypeHeader] = 'application/json';
headers[HttpHeaders.acceptLanguageHeader] = Intl.getCurrentLocale();
return headers; return headers;
} }
Map<String, String> jsonHeaders() {
var headers = defaultHeaders();
headers['Content-Type'] = 'application/json';
return headers;
}
String _authorizationHeader() { String _authorizationHeader() {
if (_token.isNotEmpty) { if (_token.isNotEmpty) {
return "Token $_token"; return "Token $_token";

View File

@ -92,7 +92,7 @@ class BarcodeHandler {
print("Scanned barcode data: ${barcode}"); print("Scanned barcode data: ${barcode}");
var data = await InvenTreeAPI().post( var response = await InvenTreeAPI().post(
url, url,
body: { body: {
"barcode": barcode, "barcode": barcode,
@ -100,19 +100,19 @@ class BarcodeHandler {
expectedStatusCode: 200 expectedStatusCode: 200
); );
if (data == null) { if (!response.isValid()) {
return; return;
} }
if (data.containsKey('error')) { if (response.data.containsKey('error')) {
_controller?.resumeCamera(); _controller?.resumeCamera();
onBarcodeUnknown(data); onBarcodeUnknown(response.data);
} else if (data.containsKey('success')) { } else if (response.data.containsKey('success')) {
_controller?.resumeCamera(); _controller?.resumeCamera();
onBarcodeMatched(data); onBarcodeMatched(response.data);
} else { } else {
_controller?.resumeCamera(); _controller?.resumeCamera();
onBarcodeUnhandled(data); onBarcodeUnhandled(response.data);
} }
} }
} }

View File

@ -150,13 +150,13 @@ class InvenTreeModel {
*/ */
Future<bool> reload() async { Future<bool> reload() async {
var response = await api.get(url, params: defaultGetFilters()); var response = await api.get(url, params: defaultGetFilters(), expectedStatusCode: 200);
if (response == null) { if (!response.isValid()) {
return false; return false;
} }
jsondata = response; jsondata = response.data;
return true; return true;
} }
@ -164,19 +164,21 @@ class InvenTreeModel {
// POST data to update the model // POST data to update the model
Future<bool> update({Map<String, String> values = const {}}) async { Future<bool> update({Map<String, String> values = const {}}) async {
var addr = path.join(URL, pk.toString()); var url = path.join(URL, pk.toString());
if (!addr.endsWith("/")) { if (!url.endsWith("/")) {
addr += "/"; url += "/";
} }
var response = await api.patch( var response = await api.patch(
addr, url,
body: values, body: values,
expectedStatusCode: 200 expectedStatusCode: 200
); );
if (response == null) return false; if (!response.isValid()) {
return false;
}
return true; return true;
} }
@ -200,15 +202,13 @@ class InvenTreeModel {
params[key] = filters[key] ?? ''; params[key] = filters[key] ?? '';
} }
print("GET: $url ${params.toString()}");
var response = await api.get(url, params: params); var response = await api.get(url, params: params);
if (response == null) { if (!response.isValid()) {
return null; return null;
} }
return createFromJson(response); return createFromJson(response.data);
} }
Future<InvenTreeModel?> create(Map<String, dynamic> data) async { Future<InvenTreeModel?> create(Map<String, dynamic> data) async {
@ -225,11 +225,12 @@ class InvenTreeModel {
var response = await api.post(URL, body: data); var response = await api.post(URL, body: data);
if (response == null) { // Invalid response returned from server
if (!response.isValid()) {
return null; return null;
} }
return createFromJson(response); return createFromJson(response.data);
} }
Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async { Future<InvenTreePageResponse?> listPaginated(int limit, int offset, {Map<String, String> filters = const {}}) async {
@ -244,19 +245,19 @@ class InvenTreeModel {
var response = await api.get(URL, params: params); var response = await api.get(URL, params: params);
if (response == null) { if (!response.isValid()) {
return null; return null;
} }
// Construct the response // Construct the response
InvenTreePageResponse page = new InvenTreePageResponse(); InvenTreePageResponse page = new InvenTreePageResponse();
if (response.containsKey("count") && response.containsKey("results")) { if (response.data.containsKey("count") && response.data.containsKey("results")) {
page.count = response["count"] as int; page.count = response.data["count"] as int;
page.results = []; page.results = [];
for (var result in response["results"]) { for (var result in response.data["results"]) {
page.addResult(createFromJson(result)); page.addResult(createFromJson(result));
} }
@ -283,7 +284,7 @@ class InvenTreeModel {
// A list of "InvenTreeModel" items // A list of "InvenTreeModel" items
List<InvenTreeModel> results = []; List<InvenTreeModel> results = [];
if (response == null) { if (!response.isValid()) {
return results; return results;
} }
@ -291,7 +292,7 @@ class InvenTreeModel {
// - No data receieved // - No data receieved
// - Data is not a list of maps // - Data is not a list of maps
for (var d in response) { for (var d in response.data) {
// 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);