using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using TMDbLib.Objects.Exceptions; using TMDbLib.Utilities.Serializer; namespace TMDbLib.Rest { internal class RestRequest { private readonly RestClient _client; private readonly string _endpoint; private object _bodyObj; private List> _queryString; private List> _urlSegment; public RestRequest(RestClient client, string endpoint) { _client = client; _endpoint = endpoint; } public RestRequest AddParameter(KeyValuePair pair, ParameterType type = ParameterType.QueryString) { AddParameter(pair.Key, pair.Value, type); return this; } public RestRequest AddParameter(string key, string value, ParameterType type = ParameterType.QueryString) { switch (type) { case ParameterType.QueryString: return AddQueryString(key, value); case ParameterType.UrlSegment: return AddUrlSegment(key, value); default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public RestRequest AddQueryString(string key, string value) { if (_queryString == null) _queryString = new List>(); _queryString.Add(new KeyValuePair(key, value)); return this; } public RestRequest AddUrlSegment(string key, string value) { if (_urlSegment == null) _urlSegment = new List>(); _urlSegment.Add(new KeyValuePair(key, value)); return this; } private void AppendQueryString(StringBuilder sb, string key, string value) { if (sb.Length > 0) sb.Append("&"); sb.Append(key); sb.Append("="); sb.Append(WebUtility.UrlEncode(value)); } private void AppendQueryString(StringBuilder sb, KeyValuePair value) { AppendQueryString(sb, value.Key, value.Value); } public async Task Delete(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Delete, cancellationToken).ConfigureAwait(false); return new RestResponse(resp); } [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created", Justification = "resp is disposed by RestResponse<>()")] public async Task> Delete(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Delete, cancellationToken).ConfigureAwait(false); return new RestResponse(resp, _client); } public async Task Get(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Get, cancellationToken).ConfigureAwait(false); return new RestResponse(resp); } [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created", Justification = "resp is disposed by RestResponse<>()")] public async Task> Get(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Get, cancellationToken).ConfigureAwait(false); return new RestResponse(resp, _client); } public async Task Post(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Post, cancellationToken).ConfigureAwait(false); return new RestResponse(resp); } [SuppressMessage("IDisposableAnalyzers.Correctness", "IDISP001:Dispose created", Justification = "resp is disposed by RestResponse<>()")] public async Task> Post(CancellationToken cancellationToken) { HttpResponseMessage resp = await SendInternal(HttpMethod.Post, cancellationToken).ConfigureAwait(false); return new RestResponse(resp, _client); } private HttpRequestMessage PrepRequest(HttpMethod method) { StringBuilder queryStringSb = new StringBuilder(); // Query String if (_queryString != null) { foreach (KeyValuePair pair in _queryString) AppendQueryString(queryStringSb, pair); } foreach (KeyValuePair pair in _client.DefaultQueryString) AppendQueryString(queryStringSb, pair); // Url string endpoint = _endpoint; if (_urlSegment != null) { foreach (KeyValuePair pair in _urlSegment) endpoint = endpoint.Replace("{" + pair.Key + "}", pair.Value); } // Build UriBuilder builder = new UriBuilder(new Uri(_client.BaseUrl, endpoint)); builder.Query = queryStringSb.ToString(); HttpRequestMessage req = new HttpRequestMessage(method, builder.Uri); // Body if (method == HttpMethod.Post && _bodyObj != null) { byte[] bodyBytes = _client.Serializer.SerializeToBytes(_bodyObj); req.Content = new ByteArrayContent(bodyBytes); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } return req; } private async Task SendInternal(HttpMethod method, CancellationToken cancellationToken) { // Account for the following settings: // - MaxRetryCount Max times to retry int timesToTry = _client.MaxRetryCount + 1; RetryConditionHeaderValue retryHeader; TMDbStatusMessage statusMessage; Debug.Assert(timesToTry >= 1); do { using HttpRequestMessage req = PrepRequest(method); HttpResponseMessage resp = await _client.HttpClient.SendAsync(req, cancellationToken).ConfigureAwait(false); bool isJson = resp.Content.Headers.ContentType.MediaType.Equals("application/json"); if (resp.IsSuccessStatusCode && isJson) #pragma warning disable IDISP011 // Don't return disposed instance return resp; #pragma warning restore IDISP011 // Don't return disposed instance try { if (isJson) statusMessage = JsonConvert.DeserializeObject(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); else statusMessage = null; switch (resp.StatusCode) { case (HttpStatusCode)429: // The previous result was a ratelimit, read the Retry-After header and wait the allotted time retryHeader = resp.Headers.RetryAfter; TimeSpan? retryAfter = retryHeader?.Delta.Value; if (retryAfter.HasValue && retryAfter.Value.TotalSeconds > 0) await Task.Delay(retryAfter.Value, cancellationToken).ConfigureAwait(false); else // TMDb sometimes gives us 0-second waits, which can lead to rapid succession of requests await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); continue; case HttpStatusCode.Unauthorized: throw new UnauthorizedAccessException( "Call to TMDb returned unauthorized. Most likely the provided API key is invalid."); case HttpStatusCode.NotFound: if (_client.ThrowApiExceptions) { throw new NotFoundException(statusMessage); } else { return null; } } throw new GeneralHttpException(resp.StatusCode); } finally { resp.Dispose(); } } while (timesToTry-- > 0); // We never reached a success throw new RequestLimitExceededException(statusMessage, retryHeader?.Date, retryHeader?.Delta); } public RestRequest SetBody(object obj) { _bodyObj = obj; return this; } } }