Skip to content
New issue

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

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

Already on GitHub? # to your account

Dio request form cannot read by Laravel #2320

Open
gioVerdiansyah opened this issue Nov 3, 2024 · 1 comment
Open

Dio request form cannot read by Laravel #2320

gioVerdiansyah opened this issue Nov 3, 2024 · 1 comment
Labels
h: need triage This issue needs to be categorized s: bug Something isn't working

Comments

@gioVerdiansyah
Copy link

gioVerdiansyah commented Nov 3, 2024

Package

dio

Version

5.7.0

Operating-System

Windows

Adapter

Default Dio

Output of flutter doctor -v

[!] Flutter (Channel stable, 3.24.0, on Microsoft Windows [Version 10.0.22631.4317], locale en-US)
    • Flutter version 3.24.0 on channel stable at D:\flutter
    ! Warning: `dart` on your path resolves to D:\Program Files\Dart\dart-sdk\bin\dart.exe, which is not inside your
      current Flutter SDK checkout at D:\flutter. Consider adding D:\flutter\bin to the front of your path.
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 80c2e84975 (3 months ago), 2024-07-30 23:06:49 +0700
    • Engine revision b8800d88be
    • Dart version 3.5.0
    • DevTools version 2.37.2
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly
      to perform update checks and upgrades.

[√] Windows Version (Installed version of Windows is version 10 or higher)

[√] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    • Android SDK at D:\Users\Verdi\AppData\Local\Android\Sdk
    • Platform android-34, build-tools 34.0.0
    • ANDROID_HOME = D:\Users\Verdi\AppData\Local\Android\Sdk
    • Java binary at: D:\Program Files\Android\Android Studio\jbr\bin\java
    • Java version OpenJDK Runtime Environment (build 17.0.11+0--11852314)
    • All Android licenses accepted.

[√] Chrome - develop for the web
    • Chrome at C:\Program Files\Google\Chrome\Application\chrome.exe

[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.10.1)
    • Visual Studio at D:\Program Files\Microsoft Visual Studio\2022\Community
    • Visual Studio Community 2022 version 17.10.34928.147
    • Windows 10 SDK version 10.0.22621.0

[√] Android Studio (version 2024.1)
    • Android Studio at D:\Program Files\Android\Android Studio
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.11+0--11852314)

[√] IntelliJ IDEA Ultimate Edition (version 2024.2)
    • IntelliJ at D:\Program Files\JetBrains\IntelliJ IDEA 2024.2.1
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart

[√] VS Code (version 1.95.1)
    • VS Code at C:\Users\User\AppData\Local\Programs\Microsoft VS Code
    • Flutter extension can be installed from:
       https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[√] Connected device (4 available)
    • vivo 1904 (mobile) • XO69MZFU9PBYR8MZ • android-arm64  • Android 11 (API 30)
    • Windows (desktop)  • windows          • windows-x64    • Microsoft Windows [Version 10.0.22631.4317]
    • Chrome (web)       • chrome           • web-javascript • Google Chrome 130.0.6723.92
    • Edge (web)         • edge             • web-javascript • Microsoft Edge 130.0.2849.56

[√] Network resources
    • All expected network resources are available.

! Doctor found issues in 1 category.

Dart Version

3.1.5

Steps to Reproduce

  1. Store the output of the file_picker library in the form of List<PlatformFile>?
void _onPickPhoto(BuildContext context, UserProfileState state) async {
    List<PlatformFile>? fileInfo = await pickFile(allowedExtensions: ['png', 'jpeg', 'jpg']);
    if (fileInfo != null) {
      context.read<UserProfileBloc>().add(UserFieldsEvent(field: 'photo', value: fileInfo));
    } else {
      AlertNotification.error(context, "Error!", messages: "Gagal mengambil file!", duration: 5);
    }
  }
  1. context.read().add(UserProfileUpdateEvent());
  2. Capture by Profile Repository
final response = await _apiClient.put(
        ApiPath.updateProfile,
        body: formData.toJson(),
      );

      print(response);
      return BaseResponse.fromJson(
        response.data, null,
      );
  1. Then the APIClient is in charge of sending the request
  2. But when sending files with this handling
Future<dynamic> _processRequestBody(dynamic body) async {
    if (body == null) return null;

    if (body is! Map<String, dynamic>) return body;

    bool hasFiles = false;
    final formMap = <String, dynamic>{};

    Future<List<MultipartFile>> processMultipleFiles(List<PlatformFile> files) async {
      return Future.wait(files.map((file) => MultipartFile.fromFile(
        file.path!,
        filename: file.name,
      )));
    }

    Future<MultipartFile> processSingleFile(PlatformFile file) async {
      final mimeType = lookupMimeType(file.path!) ?? 'application/octet-stream';
      final mediaType = DioMediaType.parse(mimeType);

      return MultipartFile.fromFile(
        file.path!,
        filename: file.name,
        contentType: mediaType
      );
    }

    for (var entry in body.entries) {
      final value = entry.value;

      try {
        if (value is List<PlatformFile>) {
          hasFiles = true;
          if (value.length == 1) {
            formMap[entry.key] = await processSingleFile(value.first);
          } else {
            formMap[entry.key] = await processMultipleFiles(value);
          }
        } else if (value is PlatformFile) {
          hasFiles = true;
          formMap[entry.key] = await processSingleFile(value);
        } else if (value != null) {
          formMap[entry.key] = value.toString();
        }
      } catch (e) {
        print('Error processing field ${entry.key}: $e');
        rethrow;
      }
    }

    print(formMap);
    return hasFiles ? FormData.fromMap(formMap) : body;
  }
  1. and sent to the laravel server
  2. catch by method
 public function editProfileUser(Request $request)
    {
        \Log::info('==== NEW REQUEST ====');
        \Log::info('Content-Type: ' . $request->header('Content-Type'));
        \Log::info('All request data:', $request->all());
        \Log::info('Files:', $request->allFiles());
        \Log::info('Has file test:', [
            'hasFile' => $request->hasFile('photo'),
            'files' => $request->files->all()
        ]);
        \Log::info($request->getContent());

        return $this->response(500, "Error", null);
        // return $this->userService->handleEditProfileUser($request->all());
    }
  1. In Larvel log
[2024-11-03 15:18:56] local.INFO: ==== NEW REQUEST ====  
[2024-11-03 15:18:56] local.INFO: Content-Type: multipart/form-data; boundary=--dio-boundary-2169289205
[2024-11-03 15:18:56] local.INFO: All request data:
[2024-11-03 15:18:56] local.INFO: Files:
[2024-11-03 15:18:56] local.INFO: Has file test: {"hasFile":false,"files":[]}
[2024-11-03 15:18:56] local.INFO: ----dio-boundary-2169289205
content-disposition: form-data; name="name"

verdiii
----dio-boundary-2169289205
content-disposition: form-data; name="email"

qwertyuio@gmail.com
----dio-boundary-2169289205
content-disposition: form-data; name="phone_number"

62123456789
----dio-boundary-2169289205
content-disposition: form-data; name="province"

zxcvbnmdsaasdfg
----dio-boundary-2169289205
content-disposition: form-data; name="distric_id"

9d621de5-83c8-4134-b2fe-f2dd79dd88f2
----dio-boundary-2169289205
content-disposition: form-data; name="sub_distric_id"

9d621de5-865b-41da-a20f-7dd03c3539cd
----dio-boundary-2169289205
content-disposition: form-data; name="village_id"

9d621de5-88fa-4a5f-aab7-3f58703e9ff9
----dio-boundary-2169289205
content-disposition: form-data; name="address"

qwertoiur
----dio-boundary-2169289205
content-disposition: form-data; name="description"


----dio-boundary-2169289205
content-disposition: form-data; name="photo"; filename="IMG-20241025-WA0009.jpg"
content-type: image/jpeg

Well when I $request->all() is empty but when I $request->getContent() there is nothing

Expected Result

I expect the result of $request->all() to appear, so that it can be validated and processed by Laravel.

Actual Result

[2024-11-03 15:18:56] local.INFO: All request data:
[2024-11-03 15:18:56] local.INFO: Files:
[2024-11-03 15:18:56] local.INFO: Has file test: {"hasFile":false,"files":[]}

Tasks

No tasks being tracked yet.
@gioVerdiansyah gioVerdiansyah added h: need triage This issue needs to be categorized s: bug Something isn't working labels Nov 3, 2024
@gioVerdiansyah
Copy link
Author

My api_client.dart

import 'package:dio/dio.dart';
import 'package:get_storage/get_storage.dart';
import 'package:file_picker/file_picker.dart';
import 'package:simaster_jakon/src/constants/storage_key_constant.dart';
import 'package:simaster_jakon/src/core/config/api_config.dart';
import 'package:mime/mime.dart';

class ApiClient {
  late final Dio _dio;
  final String baseUrl = ApiConfig.apiUrl;
  late final GetStorage _box;
  static bool _initialized = false;

  // Initialize GetStorage
  static Future<void> init() async {
    if (!_initialized) {
      await GetStorage.init();
      _initialized = true;
    }
  }

  ApiClient() {
    _box = GetStorage();

    _dio = Dio(
      BaseOptions(
        baseUrl: baseUrl,
        connectTimeout: const Duration(seconds: ApiConfig.connectTimeout),
        receiveTimeout: const Duration(seconds: ApiConfig.connectTimeout),
        headers: {
          'Accept': 'application/json',
          'x-api-key': ApiConfig.apiKey
        },
        validateStatus: (status) {
          return status != null && (status < 500 || status == 422);
        },
      ),
    );

    // interceptors logging
    _dio.interceptors.add(LogInterceptor(
      requestBody: true,
      responseBody: true,
    ));

    // Add token interceptor with improved authorization handling
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          final unusedAuth = options.extra['unusedAuth'] as bool? ?? false;

          if (!unusedAuth) {
            final token = _box.read<String?>(StorageKeyConstant.tokenKey);
            if (token != null && token.isNotEmpty) {
              options.headers['Authorization'] = 'Bearer $token';
            }
          } else {
            options.headers.remove('Authorization');
          }

          return handler.next(options);
        },
      ),
    );
  }

  Future<dynamic> _processRequestBody(dynamic body) async {
    if (body == null) return null;

    if (body is! Map<String, dynamic>) return body;

    bool hasFiles = false;
    final formMap = <String, dynamic>{};

    Future<List<MultipartFile>> processMultipleFiles(List<PlatformFile> files) async {
      return Future.wait(files.map((file) => MultipartFile.fromFile(
        file.path!,
        filename: file.name,
      )));
    }

    Future<MultipartFile> processSingleFile(PlatformFile file) async {
      final mimeType = lookupMimeType(file.path!) ?? 'application/octet-stream';
      final mediaType = DioMediaType.parse(mimeType);

      return MultipartFile.fromFile(
        file.path!,
        filename: file.name,
        contentType: mediaType
      );
    }

    for (var entry in body.entries) {
      final value = entry.value;

      try {
        if (value is List<PlatformFile>) {
          hasFiles = true;
          if (value.length == 1) {
            formMap[entry.key] = await processSingleFile(value.first);
          } else {
            formMap[entry.key] = await processMultipleFiles(value);
          }
        } else if (value is PlatformFile) {
          hasFiles = true;
          formMap[entry.key] = await processSingleFile(value);
        } else if (value != null) {
          formMap[entry.key] = value.toString();
        }
      } catch (e) {
        print('Error processing field ${entry.key}: $e');
        rethrow;
      }
    }

    print(formMap);
    return hasFiles ? FormData.fromMap(formMap) : body;
  }

  // GET Request
  Future<Response> get(
      String path, {
        Map<String, dynamic>? queryParameters,
        Options? options,
        bool unusedAuth = false,
      }) async {
    try {
      final requestOptions = options ?? Options();
      requestOptions.extra = {
        ...requestOptions.extra ?? {},
        'unusedAuth': unusedAuth,
      };

      final response = await _dio.get(
        path,
        queryParameters: queryParameters,
        options: requestOptions,
      );

      if (response.statusCode == 422) {
        return response;
      }

      return response;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // POST Request
  Future<Response> post(
      String path, {
        dynamic body,
        Map<String, dynamic>? queryParameters,
        Options? options,
        bool unusedAuth = false,
      }) async {
    try {
      final requestOptions = options ?? Options();
      requestOptions.extra = {
        ...requestOptions.extra ?? {},
        'unusedAuth': unusedAuth,
      };

      final processedBody = await _processRequestBody(body);

      if (processedBody is FormData) {
        requestOptions.headers = {
          ...requestOptions.headers ?? {},
          'Content-Type': 'multipart/form-data',
        };
      }

      final response = await _dio.post(
        path,
        data: processedBody,
        queryParameters: queryParameters,
        options: requestOptions,
      );
      return response;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // PUT Request
  Future<Response> put(
      String path, {
        dynamic body,
        Map<String, dynamic>? queryParameters,
        Options? options,
        bool unusedAuth = false,
      }) async {
    try {
      final requestOptions = options ?? Options();
      requestOptions.extra = {
        ...requestOptions.extra ?? {},
        'unusedAuth': unusedAuth,
      };

      final processedBody = await _processRequestBody(body);

      if (processedBody is FormData) {
        requestOptions.headers = {
          ...requestOptions.headers ?? {},
          'Content-Type': 'multipart/form-data',
        };
      }

      final response = await _dio.put(
        path,
        data: processedBody,
        queryParameters: queryParameters,
        options: requestOptions,
      );
      return response;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // DELETE Request
  Future<Response> delete(
      String path, {
        dynamic body,
        Map<String, dynamic>? queryParameters,
        Options? options,
        bool unusedAuth = false,
      }) async {
    try {
      final requestOptions = options ?? Options();
      requestOptions.extra = {
        ...requestOptions.extra ?? {},
        'unusedAuth': unusedAuth,
      };

      final response = await _dio.delete(
        path,
        data: body,
        queryParameters: queryParameters,
        options: requestOptions,
      );
      return response;
    } on DioException catch (e) {
      throw _handleError(e);
    }
  }

  // Error Handler
  Exception _handleError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.sendTimeout:
      case DioExceptionType.receiveTimeout:
        return TimeoutException('Connection timeout');

      case DioExceptionType.badResponse:
        switch (error.response?.statusCode) {
          case 400:
            return BadRequestException(error.response?.data?['message']);
          case 401:
            return UnauthorizedException(error.response?.data?['message']);
          case 403:
            return ForbiddenException(error.response?.data?['message']);
          case 404:
            return NotFoundException(error.response?.data?['message']);
          case 500:
            return ServerException(error.response?.data?['message']);
          default:
            return UnknownException("Gagal memuat data ...");
        }

      case DioExceptionType.cancel:
        return RequestCancelledException('Request cancelled');

      default:
        return UnknownException('Gagal melakukan koneksi :(');
    }
  }
}

// Custom Exceptions (unchanged)
class TimeoutException implements Exception {
  final String? message;
  TimeoutException([this.message]);
}

class BadRequestException implements Exception {
  final String? message;
  BadRequestException([this.message]);
}

class UnauthorizedException implements Exception {
  final String? message;
  UnauthorizedException([this.message]);
}

class ForbiddenException implements Exception {
  final String? message;
  ForbiddenException([this.message]);
}

class NotFoundException implements Exception {
  final String? message;
  NotFoundException([this.message]);
}

class ServerException implements Exception {
  final String? message;
  ServerException([this.message]);
}

class RequestCancelledException implements Exception {
  final String? message;
  RequestCancelledException([this.message]);
}

class UnknownException implements Exception {
  final String? message;
  UnknownException([this.message]);
}

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
h: need triage This issue needs to be categorized s: bug Something isn't working
Projects
None yet
Development

No branches or pull requests

1 participant