directus-dart icon indicating copy to clipboard operation
directus-dart copied to clipboard

TypeError on response code 204 - no data

Open Tapematch opened this issue 2 years ago • 2 comments

Hello, thanks for this useful package! I have found a bug that occurs when directus does not send any data back in a response. That happens when you set permissions for a role on a collection to create items, but not read items. I am developing an app where you can take surveys anonymously. Because there is no personal identification, my users can create collection items but never read them. If I use the createOne method directus saves it in the database correctly and returns the 204 code. Then a TypeError happens in your package, because no data is received:

E/flutter ( 6098): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: type 'String' is not a subtype of type 'int' of 'index'
E/flutter ( 6098): #0      new DirectusResponse
package:directus/…/data_classes/directus_response.dart:16
E/flutter ( 6098): #1      DirectusResponse.fromRequest
package:directus/…/data_classes/directus_response.dart:44

If you need additional information I am happy to provide more. Thanks for your consideration.

Tapematch avatar Apr 04 '22 06:04 Tapematch

Is the error occurring when you update/create your collection or when you update/create role? Are you providing converter?

I this the problem is here:

 /// directus_response.dart
 /// Constructor that converts [Dio] [Response] to [DirectusResponse].
  DirectusResponse(Response dioResponse, {ItemsConverter? converter}) {
    converter ??= MapItemsConverter();
    var data = dioResponse.data['data'];

    if (data is List) {
      throw DirectusError(message: 'List should use DirectusListResponse.');
    } else if (data is Map<String, Object?>) {
      this.data = converter.fromJson(data);
    } else {
      this.data = data;
    }
  }

If data is not Map or List it simply returns received, but that data does not fit return type, so it throws an error. Maybe we should set this.data = {"response": data } in else, so it does not return an error, but that might confuse users who expect that returned Map is created item. We can But I don't know if this should be changed in Dart SDK or in Directus, cause their API reference says that it returns created object. Maybe API should return empty object if no field is allowed to be read?

Can you try this branch? I don't have time today to test it, this is maybe dirty fix. https://github.com/apstanisic/directus-dart/tree/fix-no-data It simply allows returning nullable values for create/update endpoints. You can add package directly from GitHub https://stackoverflow.com/questions/54022704/how-to-add-a-package-from-github-in-flutter

apstanisic avatar Apr 04 '22 15:04 apstanisic

With your branch unfortunately the build fails because of a nullable error.

: Error: A value of type 'Future<DirectusResponse<DirectusSettings?>>' can't be returned from a function with return type 'Future<DirectusResponse<DirectusSettings>>' because 'DirectusSettings?' is nullable and 'DirectusSettings' isn't.
../…/settings/settings_handler.dart:24
 - 'Future' is from 'dart:async'.
- 'DirectusResponse' is from 'package:directus/src/data_classes/directus_response.dart' ('../../Tools/flutter/.pub-cache/git/directus-dart-bb639c48a0b71008e0299b63c834a874b9ee5aab/lib/src/data_classes/directus_response.dart').
package:directus/…/data_classes/directus_response.dart:1
- 'DirectusSettings' is from 'package:directus/src/modules/settings/directus_settings.dart' ('../../Tools/flutter/.pub-cache/git/directus-dart-bb639c48a0b71008e0299b63c834a874b9ee5aab/lib/src/modules/settings/directus_settings.dart').
package:directus/…/settings/directus_settings.dart:1
      items.updateOne(data: data, id: '1');

Sadly I currently have no time to look into it myself. Directus-wise I think it makes sense to return nothing if you dont't even have the permissions to access the item. Like with readMany you dont get an array of empty objects but an empty array.

Tapematch avatar Apr 07 '22 12:04 Tapematch

Can you check 0.9.1, it should fix it. Reopen if error still exists.

apstanisic avatar Jan 10 '23 18:01 apstanisic

@apstanisic Could you please re-open this issue, it is still relevant.

You can replicate this issue by changing the permissions so the public can create items in a collection, but not read them. If you do so, Directus will return an empty response after creating the item, which looks like this:

image

The precise error that you get when calling createOne is:

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: NoSuchMethodError: The method '[]' was called on null.
Receiver: null
Tried calling: []("data")
#0      Object.noSuchMethod (dart:core-patch/object_patch.dart:38:5)
#1      new DirectusResponse (package:directus_core/src/data_classes/directus_response.dart:16:32)
#2      DirectusResponse.fromRequest (package:directus_core/src/data_classes/directus_response.dart:44:14)
<asynchronous suspension>

I'm trying this via custom collection:

CabinetActionTriggersHandler(client: client).createOne(CabinetActionTrigger(cabinetId: "000", action: "whatever"));

Here are my module files (same fashion like directus-dart modules):

cabinet_action_trigger.dart

import 'package:json_annotation/json_annotation.dart';

part 'cabinet_action_trigger.g.dart';

// ignore:unused_element
const String _tag = "modules/cabinet_action_triggers/cabinet_action_trigger";

@JsonSerializable(fieldRename: FieldRename.snake, includeIfNull: false)
class CabinetActionTrigger {
  CabinetActionTrigger({
    this.cabinet,
    this.action,
    this.dateExecution,
  });

  factory CabinetActionTrigger.fromJson(Map<String, Object?> json) =>
      _$CabinetActionTriggerFromJson(json);

  String? cabinet;
  String? action;
  DateTime? dateExecution;

  Map<String, Object?> toJson() => _$CabinetActionTriggerToJson(this);
}

cabinet_action_trigger_converter.dart

import 'dart:convert';

import 'package:directus_core/directus_core.dart';

import 'cabinet_action_trigger.dart';

// ignore:unused_element
const String _tag =
    "modules/cabinet_action_triggers/cabinet_action_trigger_converter";

class CabinetActionTriggerConverter
    implements ItemsConverter<CabinetActionTrigger> {
  @override
  Map<String, Object?> toJson(CabinetActionTrigger data) {
    return data.toJson();
  }

  @override
  CabinetActionTrigger fromJson(Map<String, Object?> data) {
    return CabinetActionTrigger.fromJson(data);
  }
}

cabinet_action_triggers_handler.dart

import 'package:dio/dio.dart';
import 'package:directus_core/directus_core.dart';
import 'package:grow_father/modules/cabinet_action_triggers/cabinet_action_trigger.dart';
import 'package:grow_father/modules/cabinet_action_triggers/cabinet_action_trigger_converter.dart';

// ignore:unused_element
const String _tag =
    "modules/cabinet_action_triggers/cabinet_action_triggers_handler";

class CabinetActionTriggersHandler extends ItemsHandler<CabinetActionTrigger> {
  CabinetActionTriggersHandler({required Dio client})
      : super(
          'cabinet_action_trigger',
          client: client,
          converter: CabinetActionTriggerConverter(),
        );
}

The only way I found to bypass this issue is to add a createOneQuietly function in the ItemHandler that re-implements the createOne function, but ignores the response:

const String _collection = 'cabinet_action_triggers';

class CabinetActionTriggersHandler extends ItemsHandler<CabinetActionTrigger> {
  CabinetActionTriggersHandler({required Dio client})
      : super(
          _collection,
          client: client,
          converter: CabinetActionTriggerConverter(),
        );

  Future<void> createOneQuietly(CabinetActionTrigger data) async {
    const String path = "items/$_collection";
    await client.post<void>(path, data: data.toJson());
  }
}

martin-braun avatar Jun 08 '24 05:06 martin-braun