1 // -----------------------------------------------------------------------
2 // <copyright file="RestClient.cs" company="Microsoft">
3 // TODO: Update copyright text.
5 // -----------------------------------------------------------------------
7 using System.Collections.Specialized;
8 using System.Diagnostics;
9 using System.Diagnostics.Contracts;
12 using System.Runtime.Serialization;
13 using System.Threading.Tasks;
17 namespace Pithos.Network
20 using System.Collections.Generic;
25 /// TODO: Update summary.
27 public class RestClient:WebClient
29 public int Timeout { get; set; }
31 public bool TimedOut { get; set; }
33 public HttpStatusCode StatusCode { get; private set; }
35 public string StatusDescription { get; set; }
37 public long? RangeFrom { get; set; }
38 public long? RangeTo { get; set; }
40 public int Retries { get; set; }
42 private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
43 public Dictionary<string, string> Parameters
47 Contract.Ensures(_parameters!=null);
52 private static readonly ILog Log = LogManager.GetLogger("RestClient");
55 [ContractInvariantMethod]
56 private void Invariants()
58 Contract.Invariant(Headers!=null);
61 public RestClient():base()
67 public RestClient(RestClient other)
71 throw new ArgumentNullException("other");
72 Contract.EndContractBlock();
75 Timeout = other.Timeout;
76 Retries = other.Retries;
77 BaseAddress = other.BaseAddress;
79 foreach (var parameter in other.Parameters)
81 Parameters.Add(parameter.Key,parameter.Value);
84 this.Proxy = other.Proxy;
88 protected override WebRequest GetWebRequest(Uri address)
91 var webRequest = base.GetWebRequest(address);
92 var request = (HttpWebRequest)webRequest;
93 request.ServicePoint.ConnectionLimit = 10;
94 if (IfModifiedSince.HasValue)
95 request.IfModifiedSince = IfModifiedSince.Value;
96 request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
98 request.Timeout = Timeout;
100 if (RangeFrom.HasValue)
102 if (RangeTo.HasValue)
103 request.AddRange(RangeFrom.Value, RangeTo.Value);
105 request.AddRange(RangeFrom.Value);
110 public DateTime? IfModifiedSince { get; set; }
112 //Asynchronous version
113 protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
115 Log.InfoFormat("ASYNC [{0}] {1}",request.Method, request.RequestUri);
116 HttpWebResponse response = null;
120 response = (HttpWebResponse)base.GetWebResponse(request, result);
122 catch (WebException exc)
124 if (!TryGetResponse(exc, out response))
128 StatusCode = response.StatusCode;
129 LastModified = response.LastModified;
130 StatusDescription = response.StatusDescription;
136 //Synchronous version
137 protected override WebResponse GetWebResponse(WebRequest request)
139 HttpWebResponse response = null;
142 response = (HttpWebResponse)base.GetWebResponse(request);
144 catch (WebException exc)
146 if (!TryGetResponse(exc, out response))
150 StatusCode = response.StatusCode;
151 LastModified = response.LastModified;
152 StatusDescription = response.StatusDescription;
156 private bool TryGetResponse(WebException exc, out HttpWebResponse response)
159 //Fail on empty response
160 if (exc.Response == null)
163 response = (exc.Response as HttpWebResponse);
164 //Succeed on allowed status codes
165 if (AllowedStatusCodes.Contains(response.StatusCode))
168 //Does the response have any content to log?
169 if (exc.Response.ContentLength > 0)
171 var content = LogContent(exc.Response);
172 Log.ErrorFormat(content);
177 private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
179 public List<HttpStatusCode> AllowedStatusCodes
183 return _allowedStatusCodes;
187 public DateTime LastModified { get; private set; }
189 private static string LogContent(WebResponse webResponse)
191 if (webResponse == null)
192 throw new ArgumentNullException("webResponse");
193 Contract.EndContractBlock();
195 //The response stream must be copied to avoid affecting other code by disposing of the
196 //original response stream.
197 var stream = webResponse.GetResponseStream();
198 using(var memStream=new MemoryStream((int) stream.Length))
199 using (var reader = new StreamReader(memStream))
201 stream.CopyTo(memStream);
202 string content = reader.ReadToEnd();
204 stream.Seek(0,SeekOrigin.Begin);
209 public string DownloadStringWithRetry(string address,int retries=0)
213 throw new ArgumentNullException("address");
215 var actualAddress = GetActualAddress(address);
217 TraceStart("GET",actualAddress);
219 var actualRetries = (retries == 0) ? Retries : retries;
221 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
223 var task = Retry(() =>
225 var content = base.DownloadString(uriString);
227 if (StatusCode == HttpStatusCode.NoContent)
233 var result = task.Result;
237 public void Head(string address,int retries=0)
239 AllowedStatusCodes.Add(HttpStatusCode.NotFound);
240 RetryWithoutContent(address, retries, "HEAD");
243 public void PutWithRetry(string address, int retries = 0)
245 RetryWithoutContent(address, retries, "PUT");
248 public void DeleteWithRetry(string address,int retries=0)
250 RetryWithoutContent(address, retries, "DELETE");
253 public string GetHeaderValue(string headerName,bool optional=false)
255 if (this.ResponseHeaders==null)
256 throw new InvalidOperationException("ResponseHeaders are null");
257 Contract.EndContractBlock();
259 var values=this.ResponseHeaders.GetValues(headerName);
265 //A required header was not found
266 throw new WebException(String.Format("The {0} header is missing", headerName));
269 public void SetNonEmptyHeaderValue(string headerName, string value)
271 if (String.IsNullOrWhiteSpace(value))
273 Headers.Add(headerName,value);
276 private void RetryWithoutContent(string address, int retries, string method)
279 throw new ArgumentNullException("address");
281 var actualAddress = GetActualAddress(address);
282 var actualRetries = (retries == 0) ? Retries : retries;
284 var task = Retry(() =>
286 var uriString = String.Join("/",BaseAddress ,actualAddress);
287 var uri = new Uri(uriString);
288 var request = GetWebRequest(uri);
289 request.Method = method;
290 if (ResponseHeaders!=null)
291 ResponseHeaders.Clear();
293 TraceStart(method, uriString);
295 request.ContentLength = 0;
297 //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
298 //in order to return response headers
299 var response = (HttpWebResponse)GetWebResponse(request);
302 LastModified = response.LastModified;
303 StatusCode = response.StatusCode;
304 StatusDescription = response.StatusDescription;
319 catch (AggregateException ex)
321 var exc = ex.InnerException;
322 if (exc is RetryException)
324 Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
328 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
335 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
340 private static void TraceStart(string method, string actualAddress)
342 Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
345 private string GetActualAddress(string address)
347 if (Parameters.Count == 0)
349 var addressBuilder=new StringBuilder(address);
352 foreach (var parameter in Parameters)
355 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
357 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
360 return addressBuilder.ToString();
363 public string DownloadStringWithRetry(Uri address,int retries=0)
366 throw new ArgumentNullException("address");
368 var actualRetries = (retries == 0) ? Retries : retries;
369 var task = Retry(() =>
371 var content = base.DownloadString(address);
373 if (StatusCode == HttpStatusCode.NoContent)
379 var result = task.Result;
385 /// Copies headers from another RestClient
387 /// <param name="source">The RestClient from which the headers are copied</param>
388 public void CopyHeaders(RestClient source)
391 throw new ArgumentNullException("source", "source can't be null");
392 Contract.EndContractBlock();
393 //The Headers getter initializes the property, it is never null
394 Contract.Assume(Headers!=null);
396 CopyHeaders(source.Headers,Headers);
400 /// Copies headers from one header collection to another
402 /// <param name="source">The source collection from which the headers are copied</param>
403 /// <param name="target">The target collection to which the headers are copied</param>
404 public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
407 throw new ArgumentNullException("source", "source can't be null");
409 throw new ArgumentNullException("target", "target can't be null");
410 Contract.EndContractBlock();
412 for (int i = 0; i < source.Count; i++)
414 target.Add(source.GetKey(i), source[i]);
418 public void AssertStatusOK(string message)
420 if (StatusCode >= HttpStatusCode.BadRequest)
421 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
425 private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
428 throw new ArgumentNullException("original");
429 Contract.EndContractBlock();
432 tcs = new TaskCompletionSource<T>();
433 Task.Factory.StartNew(original).ContinueWith(_original =>
435 if (!_original.IsFaulted)
436 tcs.SetFromTask(_original);
439 var e = _original.Exception.InnerException;
440 var we = (e as WebException);
445 var statusCode = GetStatusCode(we);
447 //Return null for 404
448 if (statusCode == HttpStatusCode.NotFound)
449 tcs.SetResult(default(T));
450 //Retry for timeouts and service unavailable
451 else if (we.Status == WebExceptionStatus.Timeout ||
452 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
457 Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
458 tcs.SetException(new RetryException("Timed out too many times.", e));
463 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
465 Retry(original, retryCount - 1, tcs);
476 private HttpStatusCode GetStatusCode(WebException we)
479 throw new ArgumentNullException("we");
480 var statusCode = HttpStatusCode.RequestTimeout;
481 if (we.Response != null)
483 statusCode = ((HttpWebResponse) we.Response).StatusCode;
484 this.StatusCode = statusCode;
489 public UriBuilder GetAddressBuilder(string container, string objectName)
491 var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
495 public Dictionary<string, string> GetMeta(string metaPrefix)
497 if (String.IsNullOrWhiteSpace(metaPrefix))
498 throw new ArgumentNullException("metaPrefix");
499 Contract.EndContractBlock();
501 var keys = ResponseHeaders.AllKeys.AsQueryable();
502 var dict = (from key in keys
503 where key.StartsWith(metaPrefix)
504 let name = key.Substring(metaPrefix.Length)
505 select new { Name = name, Value = ResponseHeaders[key] })
506 .ToDictionary(t => t.Name, t => t.Value);
511 public class RetryException:Exception
513 public RetryException()
519 public RetryException(string message)
525 public RetryException(string message,Exception innerException)
526 :base(message,innerException)
531 public RetryException(SerializationInfo info,StreamingContext context)