@@ -23,7 +23,48 @@ class GitHub {
2323 this .endpoint = 'https://api.github.com' ,
2424 this .version = '2022-11-28' ,
2525 http.Client ? client,
26- }) : client = client ?? http.Client ();
26+ Set <String >? trustedOrigins,
27+ this .allowInsecureAuth = false ,
28+ this .retryPolicy = const RetryPolicy (),
29+ HttpTransport ? transport,
30+ }) : client = client ?? http.Client (),
31+ trustedOrigins = trustedOrigins ?? _defaultTrustedOrigins (endpoint),
32+ _transport = transport;
33+
34+ static Set <String > _defaultTrustedOrigins (String endpoint) {
35+ final origins = < String > {};
36+ final uri = Uri .tryParse (endpoint);
37+ if (uri != null && uri.hasScheme && uri.hasAuthority) {
38+ origins.add (uri.origin);
39+ } else {
40+ origins.add ('https://api.github.com' );
41+ }
42+ origins.add ('https://uploads.github.com' );
43+ return origins;
44+ }
45+
46+ /// Policy governing retries, exponential backoff, and rate limits.
47+ final RetryPolicy retryPolicy;
48+
49+ /// Underlying injectable HTTP transport.
50+ HttpTransport get transport => _transport ?? = HttpTransport (
51+ github: this ,
52+ client: client,
53+ retryPolicy: retryPolicy,
54+ trustedOrigins: trustedOrigins,
55+ allowInsecureAuth: allowInsecureAuth,
56+ endpoint: endpoint,
57+ );
58+ HttpTransport ? _transport;
59+
60+ /// Set of trusted origins permitted to receive authentication credentials.
61+ /// Defaults to the origin of [endpoint] (e.g. `https://api.github.com` ) and
62+ /// `https://uploads.github.com` .
63+ final Set <String > trustedOrigins;
64+
65+ /// Whether to allow sending credentials over unencrypted HTTP.
66+ /// Defaults to `false` for security. Set to `true` only for local test servers.
67+ final bool allowInsecureAuth;
2768
2869 static const _ratelimitLimitHeader = 'x-ratelimit-limit' ;
2970 static const _ratelimitResetHeader = 'x-ratelimit-reset' ;
@@ -51,6 +92,7 @@ class GitHub {
5192 final http.Client client;
5293
5394 ActivityService ? _activity;
95+ // ignore: deprecated_member_use_from_same_package
5496 AuthorizationsService ? _authorizations;
5597 GistsService ? _gists;
5698 GitService ? _git;
@@ -60,6 +102,7 @@ class GitHub {
60102 PullRequestsService ? _pullRequests;
61103 RepositoriesService ? _repositories;
62104 SearchService ? _search;
105+ // ignore: deprecated_member_use_from_same_package
63106 UrlShortenerService ? _urlShortener;
64107 UsersService ? _users;
65108 ChecksService ? _checks;
@@ -98,7 +141,9 @@ class GitHub {
98141 ///
99142 /// Note: You can only access this API via Basic Authentication using your
100143 /// username and password, not tokens.
144+ // ignore: deprecated_member_use_from_same_package
101145 AuthorizationsService get authorizations =>
146+ // ignore: deprecated_member_use_from_same_package
102147 _authorizations ?? = AuthorizationsService (this );
103148
104149 /// Service for gist related methods of the GitHub API.
@@ -129,7 +174,9 @@ class GitHub {
129174 SearchService get search => _search ?? = SearchService (this );
130175
131176 /// Service to provide a handy method to access GitHub's url shortener.
177+ // ignore: deprecated_member_use_from_same_package
132178 UrlShortenerService get urlShortener =>
179+ // ignore: deprecated_member_use_from_same_package
133180 _urlShortener ?? = UrlShortenerService (this );
134181
135182 /// Service for user related methods of the GitHub API.
@@ -330,10 +377,16 @@ class GitHub {
330377 fail: fail,
331378 );
332379
380+ if (response.statusCode == 204 || response.body.isEmpty) {
381+ return null as T ;
382+ }
383+
333384 final json = jsonDecode (response.body);
334385
335- final returnValue = convert (json) as T ;
336- _applyExpandos (returnValue, response);
386+ final returnValue = convert (json as S ) as T ;
387+ if (returnValue != null ) {
388+ _applyExpandos (returnValue, response);
389+ }
337390 return returnValue;
338391 }
339392
@@ -355,135 +408,27 @@ class GitHub {
355408 void Function (http.Response response)? fail,
356409 String ? preview,
357410 }) async {
358- if (rateLimitRemaining != null && rateLimitRemaining! <= 0 ) {
359- assert (rateLimitReset != null );
360- final now = DateTime .now ();
361- final waitTime = rateLimitReset! .difference (now);
362- await Future .delayed (waitTime);
363- }
364-
365411 headers ?? = < String , String > {};
366412
367413 if (preview != null ) {
368414 headers['Accept' ] = preview;
369415 }
370416
371- final authHeaderValue = auth.authorizationHeaderValue ();
372- if (authHeaderValue != null ) {
373- headers.putIfAbsent ('Authorization' , () => authHeaderValue);
374- }
375-
376- // See https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#user-agent-required
377- headers.putIfAbsent ('User-Agent' , () => auth.username ?? 'github.dart' );
378-
379- if (method == 'PUT' && body == null ) {
380- headers.putIfAbsent ('Content-Length' , () => '0' );
381- }
382-
383- var queryString = '' ;
384-
385- if (params != null ) {
386- queryString = buildQueryString (params);
387- }
388-
389- final url = StringBuffer ();
390-
391- if (path.startsWith ('http://' ) || path.startsWith ('https://' )) {
392- url.write (path);
393- url.write (queryString);
394- } else {
395- url.write (endpoint);
396- if (! path.startsWith ('/' )) {
397- url.write ('/' );
398- }
399- url.write (path);
400- url.write (queryString);
401- }
402-
403- final request = http.Request (method, Uri .parse (url.toString ()));
404- request.headers.addAll (headers);
405- if (body != null ) {
406- if (body is List <int >) {
407- request.bodyBytes = body;
408- } else {
409- request.body = body.toString ();
410- }
411- }
412-
413- final streamedResponse = await client.send (request);
414-
415- final response = await http.Response .fromStream (streamedResponse);
417+ final apiRequest = ApiRequest (
418+ method: method,
419+ path: path,
420+ headers: headers,
421+ params: params ?? const {},
422+ body: body,
423+ successStatuses: statusCode != null ? {statusCode} : null ,
424+ );
416425
417- _updateRateLimit (response.headers);
418- if (statusCode != null && statusCode != response.statusCode) {
419- if (fail != null ) {
420- fail (response);
421- }
422- handleStatusCode (response);
423- } else {
424- return response;
425- }
426+ return transport.execute (apiRequest, fail: fail);
426427 }
427428
428- ///
429429 /// Internal method to handle status codes
430- ///
431430 Never handleStatusCode (http.Response response) {
432- String ? message = '' ;
433- List <Map <String , String >>? errors;
434- if (response.headers['content-type' ]! .contains ('application/json' )) {
435- try {
436- final json = jsonDecode (response.body);
437- message = json['message' ];
438- if (json['errors' ] != null ) {
439- try {
440- errors = List <Map <String , String >>.from (json['errors' ]);
441- } catch (_) {
442- errors = [
443- {'code' : json['errors' ].toString ()}
444- ];
445- }
446- }
447- } catch (ex) {
448- throw UnknownError (this , ex.toString ());
449- }
450- }
451- switch (response.statusCode) {
452- case 404 :
453- throw NotFound (this , 'Requested Resource was Not Found' );
454- case 401 :
455- throw AccessForbidden (this );
456- case 400 :
457- if (message == 'Problems parsing JSON' ) {
458- throw InvalidJSON (this , message);
459- } else if (message == 'Body should be a JSON Hash' ) {
460- throw InvalidJSON (this , message);
461- } else {
462- throw BadRequest (this );
463- }
464- case 422 :
465- final buff = StringBuffer ();
466- buff.writeln ();
467- buff.writeln (' Message: $message ' );
468- if (errors != null ) {
469- buff.writeln (' Errors:' );
470- for (final error in errors) {
471- final resource = error['resource' ];
472- final field = error['field' ];
473- final code = error['code' ];
474- buff
475- ..writeln (' Resource: $resource ' )
476- ..writeln (' Field $field ' )
477- ..write (' Code: $code ' );
478- }
479- }
480- throw ValidationFailed (this , buff.toString ());
481- case 500 :
482- case 502 :
483- case 504 :
484- throw ServerError (this , response.statusCode, message);
485- }
486- throw UnknownError (this , message);
431+ ErrorDecoder .decode (this , response);
487432 }
488433
489434 /// Disposes of this GitHub Instance.
@@ -493,16 +438,21 @@ class GitHub {
493438 client.close ();
494439 }
495440
496- void _updateRateLimit (Map <String , String > headers) {
441+ /// Updates rate limit fields from HTTP response [headers] .
442+ void updateRateLimit (Map <String , String > headers) {
497443 if (headers.containsKey (_ratelimitLimitHeader)) {
498- _rateLimitLimit = int .parse (headers[_ratelimitLimitHeader]! );
499- _rateLimitRemaining = int .parse (headers[_ratelimitRemainingHeader]! );
500- _rateLimitReset = int .parse (headers[_ratelimitResetHeader]! );
444+ _rateLimitLimit = int .tryParse (headers[_ratelimitLimitHeader] ?? '' );
445+ _rateLimitRemaining =
446+ int .tryParse (headers[_ratelimitRemainingHeader] ?? '' );
447+ _rateLimitReset = int .tryParse (headers[_ratelimitResetHeader] ?? '' );
501448 }
502449 }
503450}
504451
505- void _applyExpandos (dynamic target, http.Response response) {
452+ void _applyExpandos (Object ? target, http.Response response) {
453+ if (target == null || target is String || target is num || target is bool ) {
454+ return ;
455+ }
506456 _etagExpando[target] = response.headers['etag' ];
507457 if (response.headers['date' ] != null ) {
508458 _dateExpando[target] = http_parser.parseHttpDate (response.headers['date' ]! );
0 commit comments