Skip to content

Commit

Permalink
Merge pull request #398 from cph-cachet/392-403-error-when-trying-to-…
Browse files Browse the repository at this point in the history
…access-a-newly-created-resource

Improvement to HTTP REST handling
  • Loading branch information
bardram authored May 31, 2024
2 parents 11ca788 + 8c7454b commit b218944
Show file tree
Hide file tree
Showing 24 changed files with 3,167 additions and 766 deletions.
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

0 comments on commit b218944

Please sign in to comment.