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

Dio request form cannot read by Laravel #2320

Open
gioVerdiansyah opened this issue Nov 3, 2024 · 2 comments
Open

Dio request form cannot read by Laravel #2320

gioVerdiansyah opened this issue Nov 3, 2024 · 2 comments
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"

[email protected]
----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]);
}

@ardi27
Copy link

ardi27 commented Nov 18, 2024

final response = await _apiClient.put(
        ApiPath.updateProfile,
        body: formData.toJson(),
      );

Are you using PUT method with form-data? AFAIK you can't use form-data in PUT method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in 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

2 participants