Пример #1
0
        private async Task <GitHubResponse <T> > MakeRequest <T>(IGitHubClient client, GitHubRequest request, CancellationToken cancellationToken, GitHubRedirect redirect)
        {
            var uri         = new Uri(client.ApiRoot, request.Uri);
            var httpRequest = new HttpRequestMessage(request.Method, uri)
            {
                Content = request.CreateBodyContent(),
            };

            // Accept header
            if (!request.AcceptHeaderOverride.IsNullOrWhiteSpace())
            {
                httpRequest.Headers.Accept.Clear();
                httpRequest.Headers.Accept.ParseAdd(request.AcceptHeaderOverride);
            }
            else if (typeof(T) == typeof(byte[]))
            {
                httpRequest.Headers.Accept.Clear();
                httpRequest.Headers.Accept.ParseAdd("application/vnd.github.v3.raw");
            }

            httpRequest.Headers.Authorization = new AuthenticationHeaderValue("bearer", client.AccessToken);

            var cache = request.CacheOptions;

            if (cache?.UserId == client.UserId)
            {
                if (request.Method != HttpMethod.Get)
                {
                    throw new InvalidOperationException("Cache options are only valid on GET requests.");
                }

                httpRequest.Headers.IfModifiedSince = cache.LastModified;
                if (!cache.ETag.IsNullOrWhiteSpace())
                {
                    httpRequest.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(cache.ETag));
                }
            }

            // User agent
            httpRequest.Headers.UserAgent.Clear();
            httpRequest.Headers.UserAgent.Add(client.UserAgent);

            // For logging
            LoggingMessageProcessingHandler.SetLogDetails(
                httpRequest,
                client.UserInfo,
                $"{client.UserInfo}/{DateTime.UtcNow:o}_{client.NextRequestId()}{httpRequest.RequestUri.PathAndQuery.Replace('/', '_')}.txt",
                request.CreationDate
                );

            HttpResponseMessage response;
            var sw = Stopwatch.StartNew();

            try {
                response = await _HttpClient.SendAsync(httpRequest, cancellationToken);
            } catch (TaskCanceledException exception) {
                sw.Stop();
                exception.Report($"Request aborted for /{request.Uri} after {sw.ElapsedMilliseconds} msec [{LoggingMessageProcessingHandler.ExtractString(httpRequest, LoggingMessageProcessingHandler.LogBlobNameKey)}]");
                throw;
            }

            // Handle redirects
            switch (response.StatusCode)
            {
            case HttpStatusCode.MovedPermanently:
            case HttpStatusCode.RedirectKeepVerb:
                request = request.CloneWithNewUri(response.Headers.Location);
                return(await MakeRequest <T>(client, request, cancellationToken, new GitHubRedirect(response.StatusCode, uri, request.Uri, redirect)));

            case HttpStatusCode.Redirect:
            case HttpStatusCode.RedirectMethod:
                request        = request.CloneWithNewUri(response.Headers.Location);
                request.Method = HttpMethod.Get;
                return(await MakeRequest <T>(client, request, cancellationToken, new GitHubRedirect(response.StatusCode, uri, request.Uri, redirect)));

            default:
                break;
            }

            var result = new GitHubResponse <T>(request)
            {
                Date     = response.Headers.Date.Value,
                Redirect = redirect,
                Status   = response.StatusCode,
            };

            // Cache Headers
            result.CacheData = new GitHubCacheDetails()
            {
                UserId       = client.UserId,
                Path         = request.Path,
                AccessToken  = client.AccessToken,
                ETag         = response.Headers.ETag?.Tag,
                LastModified = response.Content?.Headers?.LastModified,
                PollInterval = response.ParseHeader("X-Poll-Interval", x => (x == null) ? TimeSpan.Zero : TimeSpan.FromSeconds(int.Parse(x))),
            };

            // Expires and Caching Max-Age
            var expires    = response.Content?.Headers?.Expires;
            var maxAgeSpan = response.Headers.CacheControl?.SharedMaxAge ?? response.Headers.CacheControl?.MaxAge;

            if (maxAgeSpan != null)
            {
                var maxAgeExpires = DateTimeOffset.UtcNow.Add(maxAgeSpan.Value);
                if (expires == null || maxAgeExpires < expires)
                {
                    expires = maxAgeExpires;
                }
            }
            result.CacheData.Expires = expires;

            // Experiment:
            var pollExpires = result.Date.Add(result.CacheData.PollInterval);

            if (result.CacheData.Expires > pollExpires)
            {
                result.CacheData.Expires = pollExpires;
            }

            // Rate Limits
            // These aren't always sent. Check for presence and fail gracefully.
            if (response.Headers.Contains("X-RateLimit-Limit"))
            {
                result.RateLimit = new GitHubRateLimit(
                    client.AccessToken,
                    response.ParseHeader("X-RateLimit-Limit", x => int.Parse(x)),
                    response.ParseHeader("X-RateLimit-Remaining", x => int.Parse(x)),
                    response.ParseHeader("X-RateLimit-Reset", x => EpochUtility.ToDateTimeOffset(int.Parse(x))));
            }

            // Abuse
            if (response.Headers.RetryAfter != null)
            {
                var after = response.Headers.RetryAfter;
                if (after.Date != null)
                {
                    result.RetryAfter = after.Date;
                }
                else if (after.Delta != null)
                {
                    result.RetryAfter = DateTimeOffset.UtcNow.Add(after.Delta.Value);
                }
            }

            // Scopes
            var scopes = response.ParseHeader <IEnumerable <string> >("X-OAuth-Scopes", x => x?.Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries));

            if (scopes != null)
            {
                result.Scopes.UnionWith(scopes);
            }

            // Pagination
            // Screw the RFC, minimally match what GitHub actually sends.
            result.Pagination = response.ParseHeader("Link", x => (x == null) ? null : GitHubPagination.FromLinkHeader(x));

            if (result.Succeeded)
            {
                if (response.StatusCode == HttpStatusCode.NoContent && typeof(T) == typeof(bool))
                {
                    // Gross special case hack for Assignable :/
                    result.Result = (T)(object)true;
                }
                else if (response.Content != null && typeof(T) == typeof(byte[]))
                {
                    // Raw byte result
                    result.Result = (T)(object)(await response.Content.ReadAsByteArrayAsync());
                }
                else if (response.Content != null && request is GitHubGraphQLRequest)
                {
                    // GraphQL
                    var resp = await response.Content.ReadAsAsync <GraphQLResponse <T> >(GraphQLSerialization.MediaTypeFormatters);

                    result.Result = resp.Data;

                    // Possible to get errors and a (partially) useful response.
                    if (resp.Errors?.Any() == true)
                    {
                        result.Error = new GitHubError()
                        {
                            // This is a gross hack.
                            Message = resp.Errors.SerializeObject(),
                        };
                    }
                }
                else if (response.Content != null)
                {
                    // JSON formatted result
                    result.Result = await response.Content.ReadAsAsync <T>(GitHubSerialization.MediaTypeFormatters);

                    // At this point each response represents a single page.
                    result.Pages = 1;
                }
            }
            else if (response.Content != null)
            {
                var mediaType = response.Content.Headers.ContentType.MediaType;
                if (mediaType.Contains("github") || mediaType.Contains("json"))
                {
                    result.Error = await response.Content.ReadAsAsync <GitHubError>(GitHubSerialization.MediaTypeFormatters);

                    result.Error.Status = response.StatusCode;
                }
                else
                {
                    // So far, these have all been nginx errors, mostly unicorns and 502s
                    // They're already logged by the LoggingMessageProcessingHandler.
                    var body = await response.Content.ReadAsStringAsync();

                    Log.Info($"Invalid GitHub Response for [{request.Uri}]:\n\n{body}");
                }
            }

            return(result);
        }