diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a593d7..8aff511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Generate models from Figma's REST API specification ([@donny-dont](https://github.com/donny-dont)) - Update to v0.40.0 of REST API specification ([@donny-dont](https://github.com/donny-dont)) - Add `Slot` support ([@donny-dont](https://github.com/donny-dont)) +- Expose additional error information from API ([@donny-dont](https://github.com/donny-dont)) - Require `sdk: ^3.8.0` ## 7.5.0 diff --git a/lib/src/client.dart b/lib/src/client.dart index 3dcb480..c3786b9 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -73,11 +73,9 @@ class FigmaClient { final uri = Uri.parse(url); return _send('GET', uri, _authHeaders).then((res) { - if (res.statusCode >= 200 && res.statusCode < 300) { - return jsonDecode(res.body); - } else { - throw FigmaException(code: res.statusCode, message: res.body); - } + _checkResponse(res); + + return jsonDecode(res.body); }); } @@ -280,11 +278,9 @@ class FigmaClient { final uri = Uri.https(base, '$version$path', query); return _send('GET', uri, _authHeaders).then((res) { - if (res.statusCode >= 200 && res.statusCode < 300) { - return jsonDecode(res.body); - } else { - throw FigmaException(code: res.statusCode, message: res.body); - } + _checkResponse(res); + + return jsonDecode(res.body); }); } @@ -297,11 +293,9 @@ class FigmaClient { final uri = Uri.https(base, '$version$path'); return _send('POST', uri, _authHeaders, body).then((res) { - if (res.statusCode >= 200 && res.statusCode < 300) { - return jsonDecode(res.body); - } else { - throw FigmaException(code: res.statusCode, message: res.body); - } + _checkResponse(res); + + return jsonDecode(res.body); }); } @@ -314,11 +308,9 @@ class FigmaClient { final uri = Uri.https(base, '$version$path'); return _send('PUT', uri, _authHeaders, body).then((res) { - if (res.statusCode >= 200 && res.statusCode < 300) { - return jsonDecode(res.body); - } else { - throw FigmaException(code: res.statusCode, message: res.body); - } + _checkResponse(res); + + return jsonDecode(res.body); }); } @@ -326,13 +318,42 @@ class FigmaClient { Future _deleteFigma(String version, String path) { final uri = Uri.https(base, '$version$path'); - return _send('DELETE', uri, _authHeaders).then((res) { - if (res.statusCode >= 200 && res.statusCode < 300) { - return; - } else { - throw FigmaException(code: res.statusCode, message: res.body); - } - }); + return _send('DELETE', uri, _authHeaders).then(_checkResponse); + } + + void _checkResponse(Response res) { + final statusCode = res.statusCode; + if (statusCode >= 200 && statusCode < 300) { + return; + } + + final errorResponse = jsonDecode(res.body)! as Map; + final message = errorResponse['message'] as String? ?? ''; + + // Rate limit errors contain additional data in the headers + if (statusCode == 429) { + final headers = res.headers; + final retryAfter = headers['Retry-After']; + + throw FigmaRateLimitException( + retryAfter: retryAfter != null ? int.parse(retryAfter) : 0, + planTier: switch (headers['X-Figma-Plan-Tier']) { + 'enterprise' => FigmaPlanTier.enterprise, + 'org' => FigmaPlanTier.org, + 'pro' => FigmaPlanTier.pro, + 'student' => FigmaPlanTier.student, + _ => FigmaPlanTier.starter, + }, + rateLimitType: headers['X-Figma-Rate-Limit-Type'] == 'high' + ? FigmaRateLimitType.high + : FigmaRateLimitType.low, + upgradeLink: headers['X-Figma-Upgrade-Link'] ?? '', + code: statusCode, + message: message, + ); + } + + throw FigmaException(code: statusCode, message: message); } Map get _authHeaders { @@ -348,11 +369,39 @@ class FigmaClient { /// An error from the [Figma API docs](https://www.figma.com/developers/api#errors). class FigmaException implements Exception { + const FigmaException({required this.code, required this.message}); + /// HTTP status code. - final int? code; + final int code; /// Error message. - final String? message; + final String message; +} + +class FigmaRateLimitException extends FigmaException { + const FigmaRateLimitException({ + required this.retryAfter, + required this.planTier, + required this.rateLimitType, + required this.upgradeLink, + required super.code, + required super.message, + }); - const FigmaException({this.code, this.message}); + /// In seconds, how long before you should retry sending the request. + final int retryAfter; + + /// The current plan tier of the resource the user is requesting. + final FigmaPlanTier planTier; + + /// The type of rate limit the user is encountering, based on their seat type. + final FigmaRateLimitType rateLimitType; + + /// A link to either the /pricing or /settings pages depending on the + /// plan/seat of the user. + final String upgradeLink; } + +enum FigmaPlanTier { enterprise, org, pro, starter, student } + +enum FigmaRateLimitType { low, high } diff --git a/lib/src/client/io.dart b/lib/src/client/io.dart index ba524e8..b593ff2 100644 --- a/lib/src/client/io.dart +++ b/lib/src/client/io.dart @@ -34,6 +34,7 @@ Future _http2( var status = 200; final buffer = []; + final responseHeaders = {}; await for (final message in stream.incomingMessages) { if (message is HeadersStreamMessage) { @@ -42,6 +43,8 @@ Future _http2( final value = utf8.decode(header.value); if (name == ':status') { status = int.parse(value); + } else { + responseHeaders[name] = value; } } } else if (message is DataStreamMessage) { @@ -51,5 +54,5 @@ Future _http2( await transport.finish(); - return Response(status, utf8.decode(buffer)); + return Response(status, responseHeaders, utf8.decode(buffer)); } diff --git a/lib/src/client/shared.dart b/lib/src/client/shared.dart index ba6ac2c..dd4f41a 100644 --- a/lib/src/client/shared.dart +++ b/lib/src/client/shared.dart @@ -13,10 +13,11 @@ typedef SendRequest = const String base = 'api.figma.com'; class Response { + const Response(this.statusCode, this.headers, this.body); + final int statusCode; + final Map headers; final String body; - - const Response(this.statusCode, this.body); } Future http( @@ -32,7 +33,11 @@ Future http( ..body = body ?? ''; final response = await client.send(request); final responseBody = await response.stream.toBytes(); - return Response(response.statusCode, utf8.decode(responseBody)); + return Response( + response.statusCode, + response.headers, + utf8.decode(responseBody), + ); } finally { client.close(); }