Skip to content

Commit ba0f5ab

Browse files
feat: MisskeyApiExceptionとMisskeyHttpClientの機能強化
- MisskeyApiExceptionにretryAfterプロパティを追加し、HTTP 429エラー時の待機時間を管理 - MisskeyHttpClientのsendメソッドを改良し、リクエストオプションの柔軟性を向上 - RequestOptionsクラスにcontentType、headers、extraプロパティを追加し、リクエストのカスタマイズを強化
1 parent 56ac576 commit ba0f5ab

3 files changed

Lines changed: 69 additions & 11 deletions

File tree

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
/// Misskey API 呼び出し時の共通例外
22
class MisskeyApiException implements Exception {
3+
/// HTTP ステータスコード
34
final int? statusCode;
4-
final String? code; // Misskey固有のエラーコード等があれば格納
5+
6+
/// Misskey固有のエラーコード等があれば格納
7+
final String? code;
8+
9+
/// エラーメッセージ(人間可読)
510
final String message;
11+
12+
/// 元例外やエラーオブジェクト
613
final Object? raw;
714

8-
const MisskeyApiException({this.statusCode, this.code, required this.message, this.raw});
15+
/// 429 Too Many Requests 時に応じるまでの推奨待機時間
16+
final Duration? retryAfter;
17+
18+
/// 共通例外コンテナ
19+
const MisskeyApiException({this.statusCode, this.code, required this.message, this.raw, this.retryAfter});
920

1021
@override
1122
String toString() =>
12-
'MisskeyApiException(statusCode: '
13-
'$statusCode, code: $code, message: $message)';
23+
'MisskeyApiException(statusCode: $statusCode, code: $code, message: $message, retryAfter: $retryAfter)';
1424
}

lib/src/core/http/misskey_http_client.dart

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

lib/src/core/http/request_options.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,27 @@ class RequestOptions {
66
/// 冪等リクエストか。true の場合のみ自動リトライ対象
77
final bool idempotent;
88

9-
const RequestOptions({this.authRequired = true, this.idempotent = false});
9+
/// このリクエストのContent-Typeを明示的に指定する。未指定時はDioが推論する
10+
final String? contentType;
11+
12+
/// このリクエスト固有の追加ヘッダ
13+
final Map<String, String> headers;
14+
15+
/// Dioの`Options.extra`に引き継ぐ追加情報
16+
final Map<String, dynamic> extra;
17+
18+
/// リクエスト単位のオプション
19+
///
20+
/// - [authRequired]: 認証必須か(デフォルト: true)
21+
/// - [idempotent]: 冪等なリクエストか(デフォルト: false)
22+
/// - [contentType]: リクエストのContent-Type(未指定時はDioが推論)
23+
/// - [headers]: リクエスト固有の追加ヘッダ
24+
/// - [extra]: Dioの`Options.extra`へ渡す追加情報
25+
const RequestOptions({
26+
this.authRequired = true,
27+
this.idempotent = false,
28+
this.contentType,
29+
this.headers = const {},
30+
this.extra = const {},
31+
});
1032
}

0 commit comments

Comments
 (0)