Skip to content

[BUG][DART-DIO] Generation of response with content type 'application/octet-stream' returns 'MultipartFile' instead of 'Stream<Unit8List>' #20682

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

Open
5 of 6 tasks
joelbrostrom opened this issue Feb 18, 2025 · 3 comments · May be fixed by #21379

Comments

@joelbrostrom
Copy link

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

When generating a Get request with content-type application/octet-stream or text/event-stream the output returns a MultipartFile instead of the expected Stream<Uint8List>.
The generated code then calls the deserialize which fails with the exception Cannot deserialize.
I expect the generated method to return Stream<Uint8List>.

openapi-generator version

openapi-generator-cli 7.12.0-SNAPSHOT

OpenAPI declaration file content or url

ymal schema:

/api/open-ai/session/{id}:
  get:
    operationId: open_ai_session_retrieve
    parameters:
    - in: path
      name: id
      schema:
        type: string
        format: uuid
      required: true
    - in: path
      name: message_id
      schema:
        type: string
        format: uuid
      required: true
    tags:
    - open-ai
    security:
    - cookieAuth: []
    responses:
      '200':
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
        description: ''

Output:

/// openAiChatSessionMessageRetrieve
  ///
  ///
  /// Parameters:
  /// * [id]
  /// * [messageId]
  /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation
  /// * [headers] - Can be used to add additional headers to the request
  /// * [extras] - Can be used to add flags to the request
  /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response
  /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress
  /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress
  ///
  /// Returns a [Future] containing a [Response] with a [MultipartFile] as data
  /// Throws [DioException] if API call or serialization fails
  Future<Response<MultipartFile>> openAiChatSessionMessageRetrieve({
    required String id,
    CancelToken? cancelToken,
    Map<String, dynamic>? headers,
    Map<String, dynamic>? extra,
    ValidateStatus? validateStatus,
    ProgressCallback? onSendProgress,
    ProgressCallback? onReceiveProgress,
  }) async {
    final _path = r'/api/open-ai/session/{id}/'
        .replaceAll('{' r'id' '}', id.toString())
    final _options = Options(
      method: r'GET',
      responseType: ResponseType.bytes,
      headers: <String, dynamic>{
        ...?headers,
      },
      extra: <String, dynamic>{
        ...?extra,
      },
      validateStatus: validateStatus,
    );

    final _response = await _dio.request<Object>(
      _path,
      options: _options,
      cancelToken: cancelToken,
      onSendProgress: onSendProgress,
      onReceiveProgress: onReceiveProgress,
    );

    MultipartFile? _responseData;

    try {
      final rawData = _response.data;
      _responseData = rawData == null
          ? null
          : deserialize<MultipartFile, MultipartFile>(rawData, 'MultipartFile',
              growable: true); // CRASH ---> deserialize cannot create MultipartFile
    } catch (error, stackTrace) {
      throw DioException(
        requestOptions: _response.requestOptions,
        response: _response,
        type: DioExceptionType.unknown,
        error: error,
        stackTrace: stackTrace,
      );
    }

    return Response<MultipartFile>(
      data: _responseData,
      headers: _response.headers,
      isRedirect: _response.isRedirect,
      requestOptions: _response.requestOptions,
      redirects: _response.redirects,
      statusCode: _response.statusCode,
      statusMessage: _response.statusMessage,
      extra: _response.extra,
    );
  }

deserializer:

final _regList = RegExp(r'^List<(.*)>$');
final _regSet = RegExp(r'^Set<(.*)>$');
final _regMap = RegExp(r'^Map<String,(.*)>$');

ReturnType deserialize<ReturnType, BaseType>(dynamic value, String targetType,
    {bool growable = true}) {
  switch (targetType) {
    case 'String':
      return '$value' as ReturnType;
    case 'int':
      return (value is int ? value : int.parse('$value')) as ReturnType;
    case 'bool':
      if (value is bool) {
        return value as ReturnType;
      }
      final valueString = '$value'.toLowerCase();
      return (valueString == 'true' || valueString == '1') as ReturnType;
    case 'double':
      return (value is double ? value : double.parse('$value')) as ReturnType;
    // Rest of models, but no MultipartFile
    default:
      RegExpMatch? match;

      if (value is List && (match = _regList.firstMatch(targetType)) != null) {
        targetType = match![1]!; // ignore: parameter_assignments
        return value
            .map<BaseType>((dynamic v) => deserialize<BaseType, BaseType>(
                v, targetType,
                growable: growable))
            .toList(growable: growable) as ReturnType;
      }
      if (value is Set && (match = _regSet.firstMatch(targetType)) != null) {
        targetType = match![1]!; // ignore: parameter_assignments
        return value
            .map<BaseType>((dynamic v) => deserialize<BaseType, BaseType>(
                v, targetType,
                growable: growable))
            .toSet() as ReturnType;
      }
      if (value is Map && (match = _regMap.firstMatch(targetType)) != null) {
        targetType = match![1]!.trim(); // ignore: parameter_assignments
        return Map<String, BaseType>.fromIterables(
          value.keys as Iterable<String>,
          value.values.map((dynamic v) => deserialize<BaseType, BaseType>(
              v, targetType,
              growable: growable)),
        ) as ReturnType;
      }
      break;
  }
  throw Exception('Cannot deserialize');
}
Generation Details

open-generator-config.ymal

# Config options for the dart-dio generator
pubName: app_api
pubVersion: 0.0.1
pubDescription: "App API"
dateLibrary: core
serializationLibrary: json_serializable
equalityCheckMethod: equatable
Steps to reproduce

run

openapi-generator generate -i openapi_docs.yaml -g dart-dio -c open-generator-config.yaml --enable-post-process-file
@mpoimer
Copy link

mpoimer commented Jun 2, 2025

Having the same issue.
Any updates on this?

@joelbrostrom
Copy link
Author

I'm afraid not.
I had to circumvent the generated endpoint and call my endpoint directly from my repository instead.

@matthewnitschke-wk
Copy link

@mpoimer @joelbrostrom Ran into a similar issue, but specifically with the non-streamed binary result

A fix for this can be found here: #21379

Could either of you test my generator changes to see if it covers the issue you're running into as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants