Skip to content

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

@joelbrostrom

Description

@joelbrostrom

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

Activity

mpoimer

mpoimer commented on Jun 2, 2025

@mpoimer

Having the same issue.
Any updates on this?

joelbrostrom

joelbrostrom commented on Jun 3, 2025

@joelbrostrom
Author

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

matthewnitschke-wk

matthewnitschke-wk commented on Jun 4, 2025

@matthewnitschke-wk
Contributor

@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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @joelbrostrom@matthewnitschke-wk@mpoimer

      Issue actions

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