Checked to ensure exceptions occuring inside continuations are propagated
[pithos-ms-client] / trunk / Pithos.Network / RestClient.cs
1 // -----------------------------------------------------------------------
2 // <copyright file="RestClient.cs" company="Microsoft">
3 // TODO: Update copyright text.
4 // </copyright>
5 // -----------------------------------------------------------------------
6
7 using System.Collections.Specialized;
8 using System.Diagnostics;
9 using System.Diagnostics.Contracts;
10 using System.IO;
11 using System.Net;
12 using System.Runtime.Serialization;
13 using System.Threading.Tasks;
14
15 namespace Pithos.Network
16 {
17     using System;
18     using System.Collections.Generic;
19     using System.Linq;
20     using System.Text;
21
22     /// <summary>
23     /// TODO: Update summary.
24     /// </summary>
25     public class RestClient:WebClient
26     {
27         public int Timeout { get; set; }
28
29         public bool TimedOut { get; set; }
30
31         public HttpStatusCode StatusCode { get; private set; }
32
33         public string StatusDescription { get; set; }
34
35         public long? RangeFrom { get; set; }
36         public long? RangeTo { get; set; }
37
38         public int Retries { get; set; }
39
40         private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
41         public Dictionary<string, string> Parameters
42         {
43             get { return _parameters; }            
44         }
45
46         public RestClient():base()
47         {
48             
49         }
50
51        
52         public RestClient(RestClient other)
53             : base()
54         {
55             CopyHeaders(other);
56             Timeout = other.Timeout;
57             Retries = other.Retries;
58             BaseAddress = other.BaseAddress;             
59
60             foreach (var parameter in other.Parameters)
61             {
62                 Parameters.Add(parameter.Key,parameter.Value);
63             }
64
65             this.Proxy = other.Proxy;
66         }
67
68         protected override WebRequest GetWebRequest(Uri address)
69         {
70             TimedOut = false;
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;
76             if(Timeout>0)
77                 request.Timeout = Timeout;
78
79             if (RangeFrom.HasValue)
80             {
81                 if (RangeTo.HasValue)
82                     request.AddRange(RangeFrom.Value, RangeTo.Value);
83                 else
84                     request.AddRange(RangeFrom.Value);
85             }
86             return request; 
87         }
88
89         public DateTime? IfModifiedSince { get; set; }
90
91         protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
92         {
93             var response = (HttpWebResponse) base.GetWebResponse(request, result);            
94             StatusCode=response.StatusCode;
95             StatusDescription=response.StatusDescription;
96             return response;
97         }
98
99
100
101         protected override WebResponse GetWebResponse(WebRequest request)
102         {
103             try
104             {                
105                 var response = (HttpWebResponse)base.GetWebResponse(request);
106                 StatusCode = response.StatusCode;
107                 LastModified=response.LastModified;                
108                 StatusDescription = response.StatusDescription;                
109                 return response;
110             }
111             catch (WebException exc)
112             {                
113                 if (exc.Response!=null)
114                 {
115                     var response = (exc.Response as HttpWebResponse);
116                     if (response.StatusCode == HttpStatusCode.NotModified)
117                         return response;
118                     if (exc.Response.ContentLength > 0)
119                     {
120                         string content = GetContent(exc.Response);
121                         Trace.TraceError(content);
122                     }
123                 }
124                 throw;
125             }
126         }
127
128         public DateTime LastModified { get; private set; }
129
130         private static string GetContent(WebResponse webResponse)
131         {
132             string content;
133             using (var stream = webResponse.GetResponseStream())
134             using (var reader = new StreamReader(stream))
135             {
136                 content = reader.ReadToEnd();
137             }
138             return content;
139         }
140
141         public string DownloadStringWithRetry(string address,int retries=0)
142         {
143             if (address == null)
144                 throw new ArgumentNullException("address");
145
146             var actualAddress = GetActualAddress(address);
147
148             TraceStart("GET",actualAddress);            
149             
150             var actualRetries = (retries == 0) ? Retries : retries;
151             
152
153             
154             var task = Retry(() =>
155             {
156                 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);                
157                 var content = base.DownloadString(uriString);
158
159                 if (StatusCode == HttpStatusCode.NoContent)
160                     return String.Empty;
161                 return content;
162
163             }, actualRetries);
164
165             var result = task.Result;
166             return result;
167         }
168
169         public void Head(string address,int retries=0)
170         {
171             RetryWithoutContent(address, retries, "HEAD");
172         }
173
174         public void PutWithRetry(string address, int retries = 0)
175         {
176             RetryWithoutContent(address, retries, "PUT");
177         }
178
179         public void DeleteWithRetry(string address,int retries=0)
180         {
181             RetryWithoutContent(address, retries, "DELETE");
182         }
183
184         public string GetHeaderValue(string headerName)
185         {
186             var values=this.ResponseHeaders.GetValues(headerName);
187             if (values == null)
188                 throw new WebException(String.Format("The {0}  header is missing", headerName));
189             else
190                 return values[0];
191         }
192
193         private void RetryWithoutContent(string address, int retries, string method)
194         {
195             if (address == null)
196                 throw new ArgumentNullException("address");
197
198             var actualAddress = GetActualAddress(address);            
199             var actualRetries = (retries == 0) ? Retries : retries;
200
201             var task = Retry(() =>
202             {
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();
209
210                 TraceStart(method, uriString);
211
212                 var response = (HttpWebResponse)GetWebResponse(request);
213                 StatusCode = response.StatusCode;
214                 StatusDescription = response.StatusDescription;                
215                 
216
217                 return 0;
218             }, actualRetries);
219
220             try
221             {
222                 task.Wait();
223             }
224             catch (AggregateException ex)
225             {
226                 var exc = ex.InnerException;
227                 if (exc is RetryException)
228                 {
229                     Trace.TraceError("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
230                 }
231                 else
232                 {
233                     Trace.TraceError("[{0}] FAILED for {1} with \n{2}", method, address, exc);
234                 }
235                 throw;
236
237             }
238             catch(Exception ex)
239             {
240                 Trace.TraceError("[{0}] FAILED for {1} with \n{2}", method, address, ex);
241                 throw;
242             }
243         }
244         
245         private static void TraceStart(string method, string actualAddress)
246         {
247             Trace.WriteLine(String.Format("[{0}] {1} {2}", method, DateTime.Now, actualAddress));
248         }
249
250         private string GetActualAddress(string address)
251         {
252             if (Parameters.Count == 0)
253                 return address;
254             var addressBuilder=new StringBuilder(address);            
255
256             bool isFirst = true;
257             foreach (var parameter in Parameters)
258             {
259                 if(isFirst)
260                     addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
261                 else
262                     addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
263                 isFirst = false;
264             }
265             return addressBuilder.ToString();
266         }
267
268         public string DownloadStringWithRetry(Uri address,int retries=0)
269         {
270             if (address == null)
271                 throw new ArgumentNullException("address");
272
273             var actualRetries = (retries == 0) ? Retries : retries;            
274             var task = Retry(() =>
275             {
276                 var content = base.DownloadString(address);
277
278                 if (StatusCode == HttpStatusCode.NoContent)
279                     return String.Empty;
280                 return content;
281
282             }, actualRetries);
283
284             var result = task.Result;
285             return result;
286         }
287
288       
289         /// <summary>
290         /// Copies headers from another RestClient
291         /// </summary>
292         /// <param name="source">The RestClient from which the headers are copied</param>
293         public void CopyHeaders(RestClient source)
294         {
295             Contract.Requires(source != null, "source can't be null");
296             if (source == null)
297                 throw new ArgumentNullException("source", "source can't be null");
298             CopyHeaders(source.Headers,Headers);
299         }
300         
301         /// <summary>
302         /// Copies headers from one header collection to another
303         /// </summary>
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)
307         {
308             Contract.Requires(source != null, "source can't be null");
309             Contract.Requires(target != null, "target can't be null");
310             if (source == null)
311                 throw new ArgumentNullException("source", "source can't be null");
312             if (target == null)
313                 throw new ArgumentNullException("target", "target can't be null");
314             for (int i = 0; i < source.Count; i++)
315             {
316                 target.Add(source.GetKey(i), source[i]);
317             }            
318         }
319
320         public void AssertStatusOK(string message)
321         {
322             if (StatusCode >= HttpStatusCode.BadRequest)
323                 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
324         }
325
326
327         private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
328         {
329             if (tcs == null)
330                 tcs = new TaskCompletionSource<T>();
331             Task.Factory.StartNew(original).ContinueWith(_original =>
332                 {
333                     if (!_original.IsFaulted)
334                         tcs.SetFromTask(_original);
335                     else 
336                     {
337                         var e = _original.Exception.InnerException;
338                         var we = (e as WebException);
339                         if (we==null)
340                             tcs.SetException(e);
341                         else
342                         {
343                             var statusCode = GetStatusCode(we);
344
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))
351                             {
352                                 TimedOut = true;
353                                 if (retryCount == 0)
354                                 {                                    
355                                     Trace.TraceError("[ERROR] Timed out too many times. \n{0}\n",e);
356                                     tcs.SetException(new RetryException("Timed out too many times.", e));                                    
357                                 }
358                                 else
359                                 {
360                                     Trace.TraceError(
361                                         "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
362                                         retryCount, e);
363                                     Retry(original, retryCount - 1, tcs);
364                                 }
365                             }
366                             else
367                                 tcs.SetException(e);
368                         }
369                     };
370                 });
371             return tcs.Task;
372         }
373
374         private HttpStatusCode GetStatusCode(WebException we)
375         {
376             var statusCode = HttpStatusCode.RequestTimeout;
377             if (we.Response != null)
378             {
379                 statusCode = ((HttpWebResponse) we.Response).StatusCode;
380                 this.StatusCode = statusCode;
381             }
382             return statusCode;
383         }
384     }
385
386     public class RetryException:Exception
387     {
388         public RetryException()
389             :base()
390         {
391             
392         }
393
394         public RetryException(string message)
395             :base(message)
396         {
397             
398         }
399
400         public RetryException(string message,Exception innerException)
401             :base(message,innerException)
402         {
403             
404         }
405
406         public RetryException(SerializationInfo info,StreamingContext context)
407             :base(info,context)
408         {
409             
410         }
411     }
412 }