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;
16 namespace Pithos.Network
19 using System.Collections.Generic;
24 /// TODO: Update summary.
26 public class RestClient:WebClient
28 public int Timeout { get; set; }
30 public bool TimedOut { get; set; }
32 public HttpStatusCode StatusCode { get; private set; }
34 public string StatusDescription { get; set; }
36 public long? RangeFrom { get; set; }
37 public long? RangeTo { get; set; }
39 public int Retries { get; set; }
41 private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
42 public Dictionary<string, string> Parameters
46 Contract.Ensures(_parameters!=null);
51 private static readonly ILog Log = LogManager.GetLogger("RestClient");
54 [ContractInvariantMethod]
55 private void Invariants()
57 Contract.Invariant(Headers!=null);
60 public RestClient():base()
66 public RestClient(RestClient other)
70 throw new ArgumentNullException("other");
71 Contract.EndContractBlock();
74 Timeout = other.Timeout;
75 Retries = other.Retries;
76 BaseAddress = other.BaseAddress;
78 foreach (var parameter in other.Parameters)
80 Parameters.Add(parameter.Key,parameter.Value);
83 this.Proxy = other.Proxy;
86 protected override WebRequest GetWebRequest(Uri address)
89 var webRequest = base.GetWebRequest(address);
90 var request = (HttpWebRequest)webRequest;
91 if (IfModifiedSince.HasValue)
92 request.IfModifiedSince = IfModifiedSince.Value;
93 request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
95 request.Timeout = Timeout;
97 if (RangeFrom.HasValue)
100 request.AddRange(RangeFrom.Value, RangeTo.Value);
102 request.AddRange(RangeFrom.Value);
107 public DateTime? IfModifiedSince { get; set; }
109 protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
111 return ProcessResponse(()=>base.GetWebResponse(request, result));
114 protected override WebResponse GetWebResponse(WebRequest request)
116 return ProcessResponse(() => base.GetWebResponse(request));
119 private WebResponse ProcessResponse(Func<WebResponse> getResponse)
123 var response = (HttpWebResponse)getResponse();
124 StatusCode = response.StatusCode;
125 LastModified = response.LastModified;
126 StatusDescription = response.StatusDescription;
129 catch (WebException exc)
131 if (exc.Response != null)
133 var response = (exc.Response as HttpWebResponse);
134 if (AllowedStatusCodes.Contains(response.StatusCode))
136 StatusCode = response.StatusCode;
137 LastModified = response.LastModified;
138 StatusDescription = response.StatusDescription;
142 if (exc.Response.ContentLength > 0)
144 string content = GetContent(exc.Response);
145 Log.ErrorFormat(content);
152 private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
153 public List<HttpStatusCode> AllowedStatusCodes
157 return _allowedStatusCodes;
161 public DateTime LastModified { get; private set; }
163 private static string GetContent(WebResponse webResponse)
165 if (webResponse == null)
166 throw new ArgumentNullException("webResponse");
167 Contract.EndContractBlock();
170 using (var stream = webResponse.GetResponseStream())
171 using (var reader = new StreamReader(stream))
173 content = reader.ReadToEnd();
178 public string DownloadStringWithRetry(string address,int retries=0)
181 throw new ArgumentNullException("address");
183 var actualAddress = GetActualAddress(address);
185 TraceStart("GET",actualAddress);
187 var actualRetries = (retries == 0) ? Retries : retries;
191 var task = Retry(() =>
193 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
194 var content = base.DownloadString(uriString);
196 if (StatusCode == HttpStatusCode.NoContent)
202 var result = task.Result;
206 public void Head(string address,int retries=0)
208 AllowedStatusCodes.Add(HttpStatusCode.NotFound);
209 RetryWithoutContent(address, retries, "HEAD");
212 public void PutWithRetry(string address, int retries = 0)
214 RetryWithoutContent(address, retries, "PUT");
217 public void DeleteWithRetry(string address,int retries=0)
219 RetryWithoutContent(address, retries, "DELETE");
222 public string GetHeaderValue(string headerName)
224 if (this.ResponseHeaders==null)
225 throw new InvalidOperationException("ResponseHeaders are null");
226 Contract.EndContractBlock();
228 var values=this.ResponseHeaders.GetValues(headerName);
230 throw new WebException(String.Format("The {0} header is missing", headerName));
235 private void RetryWithoutContent(string address, int retries, string method)
238 throw new ArgumentNullException("address");
240 var actualAddress = GetActualAddress(address);
241 var actualRetries = (retries == 0) ? Retries : retries;
243 var task = Retry(() =>
245 var uriString = String.Join("/",BaseAddress ,actualAddress);
246 var uri = new Uri(uriString);
247 var request = GetWebRequest(uri);
248 request.Method = method;
249 if (ResponseHeaders!=null)
250 ResponseHeaders.Clear();
252 TraceStart(method, uriString);
254 request.ContentLength = 0;
255 var response = (HttpWebResponse)GetWebResponse(request);
256 StatusCode = response.StatusCode;
257 StatusDescription = response.StatusDescription;
267 catch (AggregateException ex)
269 var exc = ex.InnerException;
270 if (exc is RetryException)
272 Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
276 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
283 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
288 private static void TraceStart(string method, string actualAddress)
290 Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
293 private string GetActualAddress(string address)
295 if (Parameters.Count == 0)
297 var addressBuilder=new StringBuilder(address);
300 foreach (var parameter in Parameters)
303 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
305 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
308 return addressBuilder.ToString();
311 public string DownloadStringWithRetry(Uri address,int retries=0)
314 throw new ArgumentNullException("address");
316 var actualRetries = (retries == 0) ? Retries : retries;
317 var task = Retry(() =>
319 var content = base.DownloadString(address);
321 if (StatusCode == HttpStatusCode.NoContent)
327 var result = task.Result;
333 /// Copies headers from another RestClient
335 /// <param name="source">The RestClient from which the headers are copied</param>
336 public void CopyHeaders(RestClient source)
339 throw new ArgumentNullException("source", "source can't be null");
340 Contract.EndContractBlock();
341 //The Headers getter initializes the property, it is never null
342 Contract.Assume(Headers!=null);
344 CopyHeaders(source.Headers,Headers);
348 /// Copies headers from one header collection to another
350 /// <param name="source">The source collection from which the headers are copied</param>
351 /// <param name="target">The target collection to which the headers are copied</param>
352 public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
355 throw new ArgumentNullException("source", "source can't be null");
357 throw new ArgumentNullException("target", "target can't be null");
358 Contract.EndContractBlock();
360 for (int i = 0; i < source.Count; i++)
362 target.Add(source.GetKey(i), source[i]);
366 public void AssertStatusOK(string message)
368 if (StatusCode >= HttpStatusCode.BadRequest)
369 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
373 private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
376 throw new ArgumentNullException("original");
377 Contract.EndContractBlock();
380 tcs = new TaskCompletionSource<T>();
381 Task.Factory.StartNew(original).ContinueWith(_original =>
383 if (!_original.IsFaulted)
384 tcs.SetFromTask(_original);
387 var e = _original.Exception.InnerException;
388 var we = (e as WebException);
393 var statusCode = GetStatusCode(we);
395 //Return null for 404
396 if (statusCode == HttpStatusCode.NotFound)
397 tcs.SetResult(default(T));
398 //Retry for timeouts and service unavailable
399 else if (we.Status == WebExceptionStatus.Timeout ||
400 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
405 Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
406 tcs.SetException(new RetryException("Timed out too many times.", e));
411 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
413 Retry(original, retryCount - 1, tcs);
424 private HttpStatusCode GetStatusCode(WebException we)
427 throw new ArgumentNullException("we");
428 var statusCode = HttpStatusCode.RequestTimeout;
429 if (we.Response != null)
431 statusCode = ((HttpWebResponse) we.Response).StatusCode;
432 this.StatusCode = statusCode;
437 public UriBuilder GetAddressBuilder(string container, string objectName)
439 var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
444 public class RetryException:Exception
446 public RetryException()
452 public RetryException(string message)
458 public RetryException(string message,Exception innerException)
459 :base(message,innerException)
464 public RetryException(SerializationInfo info,StreamingContext context)