//----------------------------------------------------------------------- // // Copyright (c) 2016 Mapbox. All rights reserved. // //----------------------------------------------------------------------- #if UNITY_2017_1_OR_NEWER #define UNITY #endif namespace Mapbox.Platform { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Net; using Utils; #if NETFX_CORE using System.Net.Http; using System.Threading.Tasks; #endif #if UNITY using UnityEngine.Networking; using Mapbox.Unity.Utilities; #endif /// A response from a request. public class Response { private Response() { } public IAsyncRequest Request { get; private set; } public bool RateLimitHit { get { return StatusCode.HasValue ? 429 == StatusCode.Value : false; } } /// Flag to indicate if the request was successful public bool HasError { get { return _exceptions == null ? false : _exceptions.Count > 0; } } /// Flag to indicate if the request was fullfilled from a local cache public bool LoadedFromCache; /// Flag to indicate if the request was issued before but was issued again and updated public bool IsUpdate = false; public string RequestUrl; public int? StatusCode; public string ContentType; /// Length of rate-limiting interval in seconds. https://www.mapbox.com/api-documentation/#rate-limit-headers public int? XRateLimitInterval; /// Maximum number of requests you may make in the current interval before reaching the limit. https://www.mapbox.com/api-documentation/#rate-limit-headers public long? XRateLimitLimit; /// Timestamp of when the current interval will end and the ratelimit counter is reset. https://www.mapbox.com/api-documentation/#rate-limit-headers public DateTime? XRateLimitReset; private List _exceptions; /// Exceptions that might have occured during the request. public ReadOnlyCollection Exceptions { get { return null == _exceptions ? null : _exceptions.AsReadOnly(); } } /// Messages of exceptions otherwise empty string. public string ExceptionsAsString { get { if (null == _exceptions || _exceptions.Count == 0) { return string.Empty; } return string.Join(Environment.NewLine, _exceptions.Select(e => e.Message).ToArray()); } } /// Headers of the response. public Dictionary Headers; /// Raw data fetched from the request. public byte[] Data; public void AddException(Exception ex) { if (null == _exceptions) { _exceptions = new List(); } _exceptions.Add(ex); } // TODO: we should store timestamp of the cache! public static Response FromCache(byte[] data) { Response response = new Response(); response.Data = data; response.LoadedFromCache = true; return response; } #if !NETFX_CORE && !UNITY // full .NET Framework public static Response FromWebResponse(IAsyncRequest request, HttpWebResponse apiResponse, Exception apiEx) { Response response = new Response(); response.Request = request; if (null != apiEx) { response.AddException(apiEx); } // timeout: API response is null if (null == apiResponse) { response.AddException(new Exception("No Reponse.")); } else { // https://www.mapbox.com/api-documentation/#rate-limit-headers if (null != apiResponse.Headers) { response.Headers = new Dictionary(); for (int i = 0; i < apiResponse.Headers.Count; i++) { // TODO: implement .Net Core / UWP implementation string key = apiResponse.Headers.Keys[i]; string val = apiResponse.Headers[i]; response.Headers.Add(key, val); if (key.Equals("X-Rate-Limit-Interval", StringComparison.InvariantCultureIgnoreCase)) { int limitInterval; if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.InvariantCultureIgnoreCase)) { long limitLimit; if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.InvariantCultureIgnoreCase)) { double unixTimestamp; if (double.TryParse(val, out unixTimestamp)) { response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); } } else if (key.Equals("Content-Type", StringComparison.InvariantCultureIgnoreCase)) { response.ContentType = val; } } } if (apiResponse.StatusCode != HttpStatusCode.OK) { response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.StatusDescription))); } int statusCode = (int)apiResponse.StatusCode; response.StatusCode = statusCode; if (429 == statusCode) { response.AddException(new Exception("Rate limit hit")); } if (null != apiResponse) { using (Stream responseStream = apiResponse.GetResponseStream()) { byte[] buffer = new byte[0x1000]; int bytesRead; using (MemoryStream ms = new MemoryStream()) { while (0 != (bytesRead = responseStream.Read(buffer, 0, buffer.Length))) { ms.Write(buffer, 0, bytesRead); } response.Data = ms.ToArray(); } } apiResponse.Close(); } } return response; } #endif #if NETFX_CORE && !UNITY //UWP but not Unity public static async Task FromWebResponse(IAsyncRequest request, HttpResponseMessage apiResponse, Exception apiEx) { Response response = new Response(); response.Request = request; if (null != apiEx) { response.AddException(apiEx); } // timeout: API response is null if (null == apiResponse) { response.AddException(new Exception("No Reponse.")); } else { // https://www.mapbox.com/api-documentation/#rate-limit-headers if (null != apiResponse.Headers) { response.Headers = new Dictionary(); foreach (var hdr in apiResponse.Headers) { string key = hdr.Key; string val = hdr.Value.FirstOrDefault(); response.Headers.Add(key, val); if (key.Equals("X-Rate-Limit-Interval", StringComparison.OrdinalIgnoreCase)) { int limitInterval; if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } } else if (key.Equals("X-Rate-Limit-Limit", StringComparison.OrdinalIgnoreCase)) { long limitLimit; if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } } else if (key.Equals("X-Rate-Limit-Reset", StringComparison.OrdinalIgnoreCase)) { double unixTimestamp; if (double.TryParse(val, out unixTimestamp)) { DateTime beginningOfTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); response.XRateLimitReset = beginningOfTime.AddSeconds(unixTimestamp).ToLocalTime(); } } else if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase)) { response.ContentType = val; } } } if (apiResponse.StatusCode != HttpStatusCode.OK) { response.AddException(new Exception(string.Format("{0}: {1}", apiResponse.StatusCode, apiResponse.ReasonPhrase))); } int statusCode = (int)apiResponse.StatusCode; response.StatusCode = statusCode; if (429 == statusCode) { response.AddException(new Exception("Rate limit hit")); } if (null != apiResponse) { response.Data = await apiResponse.Content.ReadAsByteArrayAsync(); } } return response; } #endif #if UNITY // within Unity or UWP build from Unity public static Response FromWebResponse(IAsyncRequest request, UnityWebRequest apiResponse, Exception apiEx) { Response response = new Response(); response.Request = request; if (null != apiEx) { response.AddException(apiEx); } // additional string.empty check for apiResponse.error: // on UWP isNetworkError is sometimes set to true despite all being well if (apiResponse.isNetworkError && !string.IsNullOrEmpty(apiResponse.error)) { response.AddException(new Exception(apiResponse.error)); } if (request.RequestType != HttpRequestType.Head) { if (null == apiResponse.downloadHandler.data) { response.AddException(new Exception("Response has no data.")); } } #if NETFX_CORE StringComparison stringComp = StringComparison.OrdinalIgnoreCase; #elif WINDOWS_UWP StringComparison stringComp = StringComparison.OrdinalIgnoreCase; #else StringComparison stringComp = StringComparison.InvariantCultureIgnoreCase; #endif Dictionary apiHeaders = apiResponse.GetResponseHeaders(); if (null != apiHeaders) { response.Headers = new Dictionary(); foreach (var apiHdr in apiHeaders) { string key = apiHdr.Key; string val = apiHdr.Value; response.Headers.Add(key, val); if (key.Equals("X-Rate-Limit-Interval", stringComp)) { int limitInterval; if (int.TryParse(val, out limitInterval)) { response.XRateLimitInterval = limitInterval; } } else if (key.Equals("X-Rate-Limit-Limit", stringComp)) { long limitLimit; if (long.TryParse(val, out limitLimit)) { response.XRateLimitLimit = limitLimit; } } else if (key.Equals("X-Rate-Limit-Reset", stringComp)) { double unixTimestamp; if (double.TryParse(val, out unixTimestamp)) { response.XRateLimitReset = UnixTimestampUtils.From(unixTimestamp); } } else if (key.Equals("Content-Type", stringComp)) { response.ContentType = val; } } } int statusCode = (int)apiResponse.responseCode; response.StatusCode = statusCode; if (statusCode != 200) { response.AddException(new Exception(string.Format("Status Code {0}", apiResponse.responseCode))); } if (429 == statusCode) { response.AddException(new Exception("Rate limit hit")); } if (request.RequestType != HttpRequestType.Head) { response.Data = apiResponse.downloadHandler.data; } return response; } #endif } }