Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvement to HTTP REST handling #398

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backends/carp_webservices/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## 3.1.0
## 3.2.0

* upgrade of carp_serialization and carp_core
* fix of issues [#392](https://github.com/cph-cachet/carp.sensing-flutter/issues/392) and [#369](https://github.com/cph-cachet/carp.sensing-flutter/issues/369).

## 3.0.1

Expand Down
3 changes: 2 additions & 1 deletion backends/carp_webservices/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ linter:
cancel_subscriptions: true
constant_identifier_names: false
depend_on_referenced_packages: true
avoid_print: false
avoid_print: true

10 changes: 5 additions & 5 deletions backends/carp_webservices/cache/upload/batch-0.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710227Z",
"start_time": "2024-05-30T13:18:31.845318Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "ambientlight"
Expand All @@ -17,7 +17,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710234Z",
"start_time": "2024-05-30T13:18:31.845325Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "ambientlight"
Expand All @@ -33,7 +33,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710242Z",
"start_time": "2024-05-30T13:18:31.845331Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand All @@ -48,7 +48,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710245Z",
"start_time": "2024-05-30T13:18:31.845333Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand All @@ -63,7 +63,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710247Z",
"start_time": "2024-05-30T13:18:31.845335Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand Down
10 changes: 5 additions & 5 deletions backends/carp_webservices/cache/upload/batch-1.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710227Z",
"start_time": "2024-05-30T13:18:31.845318Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "ambientlight"
Expand All @@ -17,7 +17,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710234Z",
"start_time": "2024-05-30T13:18:31.845325Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "ambientlight"
Expand All @@ -33,7 +33,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710242Z",
"start_time": "2024-05-30T13:18:31.845331Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand All @@ -48,7 +48,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710245Z",
"start_time": "2024-05-30T13:18:31.845333Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand All @@ -63,7 +63,7 @@
},
{
"carp_header": {
"start_time": "2024-04-21T16:15:54.710247Z",
"start_time": "2024-05-30T13:18:31.845335Z",
"data_format": {
"namespace": "dk.cachet.carp",
"name": "deviceinformation"
Expand Down
28 changes: 0 additions & 28 deletions backends/carp_webservices/example/analysis_options.yaml

This file was deleted.

9 changes: 6 additions & 3 deletions backends/carp_webservices/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,16 @@ class AppBLoC {
void dispose() async {}

Future<ActiveParticipationInvitation?> getStudyInvitation(
BuildContext context) async {
BuildContext context,
) async {
// configure a participant service based on the carp service already configured
CarpParticipationService().configureFrom(CarpService());
_invitation = await CarpParticipationService().getStudyInvitation(context);
print('CARP Study Invitation: $_invitation');
debugPrint('CARP Study Invitation: $_invitation');

// check that the app has been updated to reflect the study id and deployment id
print('Study ID: ${app.studyId}, Deployment ID: ${app.studyDeploymentId}');
debugPrint(
'Study ID: ${app.studyId}, Deployment ID: ${app.studyDeploymentId}');
return _invitation;
}
}
Expand Down
2 changes: 2 additions & 0 deletions backends/carp_webservices/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ dependencies:
carp_webservices:
path: ../

oidc: ^0.5.2

dev_dependencies:

# The following section is specific to Flutter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class CarpAuthService extends CarpAuthBaseService {
/// Before this instance can be used, it must be configured using the
/// [configure] method.
factory CarpAuthService() => _instance;

CarpAuthService.instance() : this._();

/// Is a user authenticated?
Expand Down
100 changes: 95 additions & 5 deletions backends/carp_webservices/lib/carp_services/carp_base_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,20 @@ abstract class CarpBaseService {
final String body = toJsonString(request.toJson());
_endpointName = endpointName ?? rpcEndpointName;

debug('REQUEST: $rpcEndpointUri\n$body');
http.Response response = await httpr.post(Uri.encodeFull(rpcEndpointUri),
headers: headers, body: body);
debug('REQUEST: POST $rpcEndpointUri\n$body');
http.Response response = await httpr.post(
Uri.encodeFull(rpcEndpointUri),
headers: headers,
body: body,
);
int httpStatusCode = response.statusCode;
String responseBody = response.body;
debug('RESPONSE: $httpStatusCode\n$responseBody');
// debug(
// 'RESPONSE: $httpStatusCode\n${toJsonString(json.decode(responseBody))}');

// check if this is a json list or an empty string
// if so turn it into a valid json map
// Check if this is a json list or an empty string
// If so turn it into a valid json map
if (responseBody.startsWith('[')) responseBody = '{"items":$responseBody}';
if (responseBody.isEmpty) responseBody = '{}';

Expand All @@ -115,4 +118,91 @@ abstract class CarpBaseService {
path: responseJson["path"].toString(),
);
}

/// Sends an HTTP GET request to the given [url] for this CAWS service.
Future<http.Response> _get(String url) async {
debug('REQUEST: GET $url');

var response = await httpr.get(url, headers: headers);

debug('RESPONSE: ${response.statusCode}\n${response.body}');

// If we get a 403 forbidden response try to refresh token and retry once.
// See issue : https://github.com/cph-cachet/carp.sensing-flutter/issues/392
if (response.statusCode == HttpStatus.forbidden) {
await CarpAuthService().refresh();
response = await httpr.get(url, headers: headers);
}

return _clean(response);
}

/// Sends an HTTP POST request with [body] to the given [url] for this CAWS service.
Future<http.Response> _post(String url, {Object? body}) async {
debug('REQUEST: POST $url\n$body');

var response = await httpr.post(url, headers: headers, body: body);

debug('RESPONSE: ${response.statusCode}\n${response.body}');

if (response.statusCode == HttpStatus.forbidden) {
await CarpAuthService().refresh();
response = await httpr.post(url, headers: headers, body: body);
}

return _clean(response);
}

/// Sends an HTTP PUT request with [body] to the given [url] for this CAWS service.
Future<http.Response> _put(String url, {Object? body}) async {
debug('REQUEST: PUT $url\n$body');

var response = await httpr.put(url, headers: headers, body: body);

debug('RESPONSE: ${response.statusCode}\n${response.body}');

if (response.statusCode == HttpStatus.forbidden) {
await CarpAuthService().refresh();
response = await httpr.put(url, headers: headers, body: body);
}

return _clean(response);
}

/// Sends an HTTP DELETE request to the given [url] for this CAWS service.
Future<http.Response> _delete(String url) async {
debug('REQUEST: DELETE $url');

var response = await httpr.delete(url, headers: headers);

debug('RESPONSE: ${response.statusCode}\n${response.body}');

if (response.statusCode == HttpStatus.forbidden) {
await CarpAuthService().refresh();
response = await httpr.delete(url, headers: headers);
}

return _clean(response);
}

/// Check if we get an Nginx reverse proxy error in HTML format, and if so
/// convert it to a JSON error message.
///
/// See issue : https://github.com/cph-cachet/carp.sensing-flutter/issues/369
http.Response _clean(http.Response response) =>
response.body.startsWith('<html>')
? http.Response(
'{'
'"statusCode": 502,'
'"message": "502 Bad Gateway.",'
'"path": "POST ${response.request?.url}"'
'}',
response.statusCode,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
request: response.request,
)
: response;
}
38 changes: 12 additions & 26 deletions backends/carp_webservices/lib/carp_services/carp_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

part of 'carp_services.dart';

/// Provide access to a CARP web service endpoint.
/// Provide access to a CARP Web Services (CAWS) endpoints.
///
/// The (current) assumption is that each Flutter app (using this library) will
/// only connect to one CARP web service backend.
/// only connect to one CARP web services backend.
/// Therefore a [CarpService] is a singleton and can be used like:
///
/// ```dart
Expand All @@ -35,23 +35,6 @@ class CarpService extends CarpBaseService {
@override
CarpApp get app => nonNullAble(_app);

/// The headers for any authenticated HTTP REST call to this [CarpService].
@override
Map<String, String> get headers {
if (CarpAuthService().currentUser.token == null) {
throw CarpServiceException(
message:
"OAuth token is null. Call 'CarpAuthService().authenticate()' first.");
}

return {
"Content-Type": "application/json",
"Authorization":
"bearer ${CarpAuthService().currentUser.token!.accessToken}",
"cache-control": "no-cache"
};
}

// --------------------------------------------------------------------------
// CONSENT DOCUMENT
// --------------------------------------------------------------------------
Expand All @@ -64,11 +47,13 @@ class CarpService extends CarpBaseService {
/// Returns the created [ConsentDocument] if the document is uploaded correctly.
Future<ConsentDocument> createConsentDocument(
Map<String, dynamic> document) async {
debug('REQUEST: POST $consentDocumentEndpointUri');

// POST the document to the CARP web service
http.Response response = await http.post(
Uri.parse(Uri.encodeFull(consentDocumentEndpointUri)),
headers: headers,
body: json.encode(document));
http.Response response = await _post(
consentDocumentEndpointUri,
body: json.encode(document),
);

int httpStatusCode = response.statusCode;
Map<String, dynamic> responseJson =
Expand All @@ -92,8 +77,9 @@ class CarpService extends CarpBaseService {
String url = "$consentDocumentEndpointUri/$id";

// GET the consent document from the CARP web service
http.Response response =
await httpr.get(Uri.encodeFull(url), headers: headers);
http.Response response = await _get(Uri.encodeFull(url));

debug('RESPONSE: ${response.statusCode}\n${response.body}');

int httpStatusCode = response.statusCode;
Map<String, dynamic> responseJson =
Expand Down Expand Up @@ -161,7 +147,7 @@ class CarpService extends CarpBaseService {
int httpStatusCode = response.statusCode;

switch (httpStatusCode) {
case 200:
case HttpStatus.ok:
{
List<dynamic> list = json.decode(response.body) as List<dynamic>;
List<CarpFileResponse> fileList = [];
Expand Down
11 changes: 3 additions & 8 deletions backends/carp_webservices/lib/carp_services/carp_tasks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,16 @@ class FileUploadTask extends CarpServiceTask {

request.files.add(ClonableMultipartFile.fromFileSync(file.path));

print('files # : ${request.files.length}');

httpr.send(request).then((http.StreamedResponse response) {
response.stream.toStringStream().first.then((body) {
final int httpStatusCode = response.statusCode;
print("httpStatusCode: $httpStatusCode");
print("response:\n$response");
print("body:\n$body");
final Map<String, dynamic> map =
json.decode(body) as Map<String, dynamic>;

switch (httpStatusCode) {
// CARP web service returns "201 Created" when a file is created on the server.
case 200:
case 201:
case HttpStatus.ok:
case HttpStatus.created:
{
// save the id generated from the server
reference.id = map["id"] as int;
Expand Down Expand Up @@ -152,7 +147,7 @@ class FileDownloadTask extends CarpServiceTask {
final int httpStatusCode = response.statusCode;

switch (httpStatusCode) {
case 200:
case HttpStatus.ok:
{
_state = TaskStateType.success;
file.writeAsBytes(response.bodyBytes);
Expand Down
Loading
Loading