@@ -40,7 +40,6 @@ class MisskeyHttpClient {
4040 ...config.defaultHeaders,
4141 if (config.userAgent != null ) 'User-Agent' : config.userAgent,
4242 'Accept' : 'application/json' ,
43- 'Content-Type' : 'application/json' ,
4443 },
4544 );
4645 _dio = Dio (baseOptions);
@@ -58,12 +57,17 @@ class MisskeyHttpClient {
5857 }
5958
6059 /// `path` は `/notes/create` のように `/api` より後のパスを渡す
60+ ///
61+ /// [body] には `Map<String,dynamic>` (JSON)・`FormData` (multipart)・`null` を指定可能
62+ /// [options] で `contentType` /`headers` /`extra` をリクエスト単位で上書きできる
63+ /// アップロード時は [onSendProgress] で進捗を受け取れる
6164 Future <T > send <T >(
6265 String path, {
6366 String method = 'POST' ,
64- Map < String , dynamic > ? body,
67+ dynamic body,
6568 ro.RequestOptions options = const ro.RequestOptions (),
6669 CancelToken ? cancelToken,
70+ void Function (int , int )? onSendProgress,
6771 }) async {
6872 final r = RetryOptions (
6973 maxAttempts: options.idempotent ? config.maxRetries : 1 ,
@@ -76,11 +80,18 @@ class MisskeyHttpClient {
7680 try {
7781 final result = await r.retry (
7882 () async {
83+ final reqOptions = Options (
84+ method: method,
85+ contentType: options.contentType,
86+ headers: options.headers.isEmpty ? null : Map <String , dynamic >.from (options.headers),
87+ extra: {'authRequired' : options.authRequired, ...options.extra},
88+ );
7989 final Response <dynamic > res = await _dio.request (
8090 path.startsWith ('/' ) ? path : '/$path ' ,
8191 data: body,
82- options: Options (method : method, extra : { 'authRequired' : options.authRequired}) ,
92+ options: reqOptions ,
8393 cancelToken: cancelToken,
94+ onSendProgress: onSendProgress,
8495 );
8596 return res;
8697 },
@@ -130,6 +141,7 @@ class MisskeyHttpClient {
130141 final status = e.response? .statusCode;
131142 String message = e.message ?? 'HTTP error' ;
132143 String ? code;
144+ Duration ? retryAfter;
133145 final data = e.response? .data;
134146 if (data is Map ) {
135147 final dynamic errorObj = data['error' ];
@@ -145,7 +157,14 @@ class MisskeyHttpClient {
145157 if (m != null ) message = m.toString ();
146158 }
147159 }
148- return MisskeyApiException (statusCode: status, code: code, message: message, raw: e);
160+ final ra = e.response? .headers.value ('retry-after' );
161+ if (ra != null ) {
162+ final seconds = int .tryParse (ra.trim ());
163+ if (seconds != null ) {
164+ retryAfter = Duration (seconds: seconds);
165+ }
166+ }
167+ return MisskeyApiException (statusCode: status, code: code, message: message, raw: e, retryAfter: retryAfter);
149168 }
150169}
151170
@@ -158,15 +177,22 @@ class _MisskeyInterceptor extends Interceptor {
158177
159178 @override
160179 void onRequest (RequestOptions options, RequestInterceptorHandler handler) async {
161- // 認証付与(POSTのみ、かつ body が Map のとき )
180+ // 認証付与(POSTのみ、Map/FormData/空bodyに対応 )
162181 final extra = options.extra;
163182 final authRequired = (extra['authRequired' ] as bool ? ) ?? true ;
164183 if (authRequired && options.method.toUpperCase () == 'POST' ) {
165184 final token = await tokenProvider? .call ();
166185 if (token != null && token.isNotEmpty) {
167186 final data = options.data;
168187 if (data is Map <String , dynamic >) {
169- options.data = < String , dynamic > {...data, 'i' : token};
188+ if (! data.containsKey ('i' )) {
189+ options.data = < String , dynamic > {...data, 'i' : token};
190+ }
191+ } else if (data is FormData ) {
192+ final hasI = data.fields.any ((e) => e.key == 'i' );
193+ if (! hasI) {
194+ data.fields.add (MapEntry ('i' , token));
195+ }
170196 } else if (data == null ) {
171197 options.data = < String , dynamic > {'i' : token};
172198 }
0 commit comments