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;
15 namespace Pithos.Network
18 using System.Collections.Generic;
23 /// TODO: Update summary.
25 public class RestClient:WebClient
27 public int Timeout { get; set; }
29 public bool TimedOut { get; set; }
31 public HttpStatusCode StatusCode { get; private set; }
33 public string StatusDescription { get; set; }
35 public long? RangeFrom { get; set; }
36 public long? RangeTo { get; set; }
38 public int Retries { get; set; }
40 private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
41 public Dictionary<string, string> Parameters
43 get { return _parameters; }
46 public RestClient():base()
52 public RestClient(RestClient other)
56 Timeout = other.Timeout;
57 Retries = other.Retries;
58 BaseAddress = other.BaseAddress;
60 foreach (var parameter in other.Parameters)
62 Parameters.Add(parameter.Key,parameter.Value);
65 this.Proxy = other.Proxy;
68 protected override WebRequest GetWebRequest(Uri address)
71 var webRequest = base.GetWebRequest(address);
72 var request = webRequest as HttpWebRequest;
73 if (IfModifiedSince.HasValue)
74 request.IfModifiedSince = IfModifiedSince.Value;
75 request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
77 request.Timeout = Timeout;
79 if (RangeFrom.HasValue)
82 request.AddRange(RangeFrom.Value, RangeTo.Value);
84 request.AddRange(RangeFrom.Value);
89 public DateTime? IfModifiedSince { get; set; }
91 protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
93 var response = (HttpWebResponse) base.GetWebResponse(request, result);
94 StatusCode=response.StatusCode;
95 StatusDescription=response.StatusDescription;
101 protected override WebResponse GetWebResponse(WebRequest request)
105 var response = (HttpWebResponse)base.GetWebResponse(request);
106 StatusCode = response.StatusCode;
107 LastModified=response.LastModified;
108 StatusDescription = response.StatusDescription;
111 catch (WebException exc)
113 if (exc.Response!=null)
115 var response = (exc.Response as HttpWebResponse);
116 if (response.StatusCode == HttpStatusCode.NotModified)
118 if (exc.Response.ContentLength > 0)
120 string content = GetContent(exc.Response);
121 Trace.TraceError(content);
128 public DateTime LastModified { get; private set; }
130 private static string GetContent(WebResponse webResponse)
133 using (var stream = webResponse.GetResponseStream())
134 using (var reader = new StreamReader(stream))
136 content = reader.ReadToEnd();
141 public string DownloadStringWithRetry(string address,int retries=0)
144 throw new ArgumentNullException("address");
146 var actualAddress = GetActualAddress(address);
148 TraceStart("GET",actualAddress);
150 var actualRetries = (retries == 0) ? Retries : retries;
154 var task = Retry(() =>
156 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
157 var content = base.DownloadString(uriString);
159 if (StatusCode == HttpStatusCode.NoContent)
165 var result = task.Result;
169 public void Head(string address,int retries=0)
171 RetryWithoutContent(address, retries, "HEAD");
174 public void PutWithRetry(string address, int retries = 0)
176 RetryWithoutContent(address, retries, "PUT");
179 public void DeleteWithRetry(string address,int retries=0)
181 RetryWithoutContent(address, retries, "DELETE");
184 public string GetHeaderValue(string headerName)
186 var values=this.ResponseHeaders.GetValues(headerName);
188 throw new WebException(String.Format("The {0} header is missing", headerName));
193 private void RetryWithoutContent(string address, int retries, string method)
196 throw new ArgumentNullException("address");
198 var actualAddress = GetActualAddress(address);
199 var actualRetries = (retries == 0) ? Retries : retries;
201 var task = Retry(() =>
203 var uriString = String.Join("/",BaseAddress ,actualAddress);
204 var uri = new Uri(uriString);
205 var request = GetWebRequest(uri);
206 request.Method = method;
207 if (ResponseHeaders!=null)
208 ResponseHeaders.Clear();
210 TraceStart(method, uriString);
212 var response = (HttpWebResponse)GetWebResponse(request);
213 StatusCode = response.StatusCode;
214 StatusDescription = response.StatusDescription;
224 catch (AggregateException ex)
226 var exc = ex.InnerException;
227 if (exc is RetryException)
229 Trace.TraceError("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
233 Trace.TraceError("[{0}] FAILED for {1} with \n{2}", method, address, exc);
240 Trace.TraceError("[{0}] FAILED for {1} with \n{2}", method, address, ex);
245 private static void TraceStart(string method, string actualAddress)
247 Trace.WriteLine(String.Format("[{0}] {1} {2}", method, DateTime.Now, actualAddress));
250 private string GetActualAddress(string address)
252 if (Parameters.Count == 0)
254 var addressBuilder=new StringBuilder(address);
257 foreach (var parameter in Parameters)
260 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
262 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
265 return addressBuilder.ToString();
268 public string DownloadStringWithRetry(Uri address,int retries=0)
271 throw new ArgumentNullException("address");
273 var actualRetries = (retries == 0) ? Retries : retries;
274 var task = Retry(() =>
276 var content = base.DownloadString(address);
278 if (StatusCode == HttpStatusCode.NoContent)
284 var result = task.Result;
290 /// Copies headers from another RestClient
292 /// <param name="source">The RestClient from which the headers are copied</param>
293 public void CopyHeaders(RestClient source)
295 Contract.Requires(source != null, "source can't be null");
297 throw new ArgumentNullException("source", "source can't be null");
298 CopyHeaders(source.Headers,Headers);
302 /// Copies headers from one header collection to another
304 /// <param name="source">The source collection from which the headers are copied</param>
305 /// <param name="target">The target collection to which the headers are copied</param>
306 public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
308 Contract.Requires(source != null, "source can't be null");
309 Contract.Requires(target != null, "target can't be null");
311 throw new ArgumentNullException("source", "source can't be null");
313 throw new ArgumentNullException("target", "target can't be null");
314 for (int i = 0; i < source.Count; i++)
316 target.Add(source.GetKey(i), source[i]);
320 public void AssertStatusOK(string message)
322 if (StatusCode >= HttpStatusCode.BadRequest)
323 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
327 private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
330 tcs = new TaskCompletionSource<T>();
331 Task.Factory.StartNew(original).ContinueWith(_original =>
333 if (!_original.IsFaulted)
334 tcs.SetFromTask(_original);
337 var e = _original.Exception.InnerException;
338 var we = (e as WebException);
343 var statusCode = GetStatusCode(we);
345 //Return null for 404
346 if (statusCode == HttpStatusCode.NotFound)
347 tcs.SetResult(default(T));
348 //Retry for timeouts and service unavailable
349 else if (we.Status == WebExceptionStatus.Timeout ||
350 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
355 Trace.TraceError("[ERROR] Timed out too many times. \n{0}\n",e);
356 tcs.SetException(new RetryException("Timed out too many times.", e));
361 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
363 Retry(original, retryCount - 1, tcs);
374 private HttpStatusCode GetStatusCode(WebException we)
376 var statusCode = HttpStatusCode.RequestTimeout;
377 if (we.Response != null)
379 statusCode = ((HttpWebResponse) we.Response).StatusCode;
380 this.StatusCode = statusCode;
386 public class RetryException:Exception
388 public RetryException()
394 public RetryException(string message)
400 public RetryException(string message,Exception innerException)
401 :base(message,innerException)
406 public RetryException(SerializationInfo info,StreamingContext context)