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;
87 protected override WebRequest GetWebRequest(Uri address)
90 var webRequest = base.GetWebRequest(address);
91 var request = (HttpWebRequest)webRequest;
92 if (IfModifiedSince.HasValue)
93 request.IfModifiedSince = IfModifiedSince.Value;
94 request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
96 request.Timeout = Timeout;
98 if (RangeFrom.HasValue)
100 if (RangeTo.HasValue)
101 request.AddRange(RangeFrom.Value, RangeTo.Value);
103 request.AddRange(RangeFrom.Value);
108 public DateTime? IfModifiedSince { get; set; }
110 protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
112 return ProcessResponse(()=>base.GetWebResponse(request, result));
115 protected override WebResponse GetWebResponse(WebRequest request)
117 return ProcessResponse(() => base.GetWebResponse(request));
120 private WebResponse ProcessResponse(Func<WebResponse> getResponse)
124 var response = (HttpWebResponse)getResponse();
125 StatusCode = response.StatusCode;
126 LastModified = response.LastModified;
127 StatusDescription = response.StatusDescription;
130 catch (WebException exc)
132 if (exc.Response != null)
134 var response = (exc.Response as HttpWebResponse);
135 if (AllowedStatusCodes.Contains(response.StatusCode))
137 StatusCode = response.StatusCode;
138 LastModified = response.LastModified;
139 StatusDescription = response.StatusDescription;
143 if (exc.Response.ContentLength > 0)
145 string content = GetContent(exc.Response);
146 Log.ErrorFormat(content);
153 private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
154 public List<HttpStatusCode> AllowedStatusCodes
158 return _allowedStatusCodes;
162 public DateTime LastModified { get; private set; }
164 private static string GetContent(WebResponse webResponse)
166 if (webResponse == null)
167 throw new ArgumentNullException("webResponse");
168 Contract.EndContractBlock();
171 using (var stream = webResponse.GetResponseStream())
172 using (var reader = new StreamReader(stream))
174 content = reader.ReadToEnd();
179 public string DownloadStringWithRetry(string address,int retries=0)
183 throw new ArgumentNullException("address");
185 var actualAddress = GetActualAddress(address);
187 TraceStart("GET",actualAddress);
189 var actualRetries = (retries == 0) ? Retries : retries;
192 var task = Retry(() =>
194 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
195 var content = base.DownloadString(uriString);
197 if (StatusCode == HttpStatusCode.NoContent)
203 var result = task.Result;
207 public void Head(string address,int retries=0)
209 AllowedStatusCodes.Add(HttpStatusCode.NotFound);
210 RetryWithoutContent(address, retries, "HEAD");
213 public void PutWithRetry(string address, int retries = 0)
215 RetryWithoutContent(address, retries, "PUT");
218 public void DeleteWithRetry(string address,int retries=0)
220 RetryWithoutContent(address, retries, "DELETE");
223 public string GetHeaderValue(string headerName,bool optional=false)
225 if (this.ResponseHeaders==null)
226 throw new InvalidOperationException("ResponseHeaders are null");
227 Contract.EndContractBlock();
229 var values=this.ResponseHeaders.GetValues(headerName);
235 //A required header was not found
236 throw new WebException(String.Format("The {0} header is missing", headerName));
239 public void SetNonEmptyHeaderValue(string headerName, string value)
241 if (String.IsNullOrWhiteSpace(value))
243 Headers.Add(headerName,value);
246 private void RetryWithoutContent(string address, int retries, string method)
249 throw new ArgumentNullException("address");
251 var actualAddress = GetActualAddress(address);
252 var actualRetries = (retries == 0) ? Retries : retries;
254 var task = Retry(() =>
256 var uriString = String.Join("/",BaseAddress ,actualAddress);
257 var uri = new Uri(uriString);
258 var request = GetWebRequest(uri);
259 request.Method = method;
260 if (ResponseHeaders!=null)
261 ResponseHeaders.Clear();
263 TraceStart(method, uriString);
265 request.ContentLength = 0;
266 var response = (HttpWebResponse)GetWebResponse(request);
267 StatusCode = response.StatusCode;
268 StatusDescription = response.StatusDescription;
278 catch (AggregateException ex)
280 var exc = ex.InnerException;
281 if (exc is RetryException)
283 Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
287 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
294 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
299 private static void TraceStart(string method, string actualAddress)
301 Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
304 private string GetActualAddress(string address)
306 if (Parameters.Count == 0)
308 var addressBuilder=new StringBuilder(address);
311 foreach (var parameter in Parameters)
314 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
316 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
319 return addressBuilder.ToString();
322 public string DownloadStringWithRetry(Uri address,int retries=0)
325 throw new ArgumentNullException("address");
327 var actualRetries = (retries == 0) ? Retries : retries;
328 var task = Retry(() =>
330 var content = base.DownloadString(address);
332 if (StatusCode == HttpStatusCode.NoContent)
338 var result = task.Result;
344 /// Copies headers from another RestClient
346 /// <param name="source">The RestClient from which the headers are copied</param>
347 public void CopyHeaders(RestClient source)
350 throw new ArgumentNullException("source", "source can't be null");
351 Contract.EndContractBlock();
352 //The Headers getter initializes the property, it is never null
353 Contract.Assume(Headers!=null);
355 CopyHeaders(source.Headers,Headers);
359 /// Copies headers from one header collection to another
361 /// <param name="source">The source collection from which the headers are copied</param>
362 /// <param name="target">The target collection to which the headers are copied</param>
363 public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
366 throw new ArgumentNullException("source", "source can't be null");
368 throw new ArgumentNullException("target", "target can't be null");
369 Contract.EndContractBlock();
371 for (int i = 0; i < source.Count; i++)
373 target.Add(source.GetKey(i), source[i]);
377 public void AssertStatusOK(string message)
379 if (StatusCode >= HttpStatusCode.BadRequest)
380 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
384 private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
387 throw new ArgumentNullException("original");
388 Contract.EndContractBlock();
391 tcs = new TaskCompletionSource<T>();
392 Task.Factory.StartNew(original).ContinueWith(_original =>
394 if (!_original.IsFaulted)
395 tcs.SetFromTask(_original);
398 var e = _original.Exception.InnerException;
399 var we = (e as WebException);
404 var statusCode = GetStatusCode(we);
406 //Return null for 404
407 if (statusCode == HttpStatusCode.NotFound)
408 tcs.SetResult(default(T));
409 //Retry for timeouts and service unavailable
410 else if (we.Status == WebExceptionStatus.Timeout ||
411 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
416 Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
417 tcs.SetException(new RetryException("Timed out too many times.", e));
422 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
424 Retry(original, retryCount - 1, tcs);
435 private HttpStatusCode GetStatusCode(WebException we)
438 throw new ArgumentNullException("we");
439 var statusCode = HttpStatusCode.RequestTimeout;
440 if (we.Response != null)
442 statusCode = ((HttpWebResponse) we.Response).StatusCode;
443 this.StatusCode = statusCode;
448 public UriBuilder GetAddressBuilder(string container, string objectName)
450 var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
455 public class RetryException:Exception
457 public RetryException()
463 public RetryException(string message)
469 public RetryException(string message,Exception innerException)
470 :base(message,innerException)
475 public RetryException(SerializationInfo info,StreamingContext context)