2 /* -----------------------------------------------------------------------
\r
3 * <copyright file="RestClient.cs" company="GRNet">
\r
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
\r
7 * Redistribution and use in source and binary forms, with or
\r
8 * without modification, are permitted provided that the following
\r
9 * conditions are met:
\r
11 * 1. Redistributions of source code must retain the above
\r
12 * copyright notice, this list of conditions and the following
\r
15 * 2. Redistributions in binary form must reproduce the above
\r
16 * copyright notice, this list of conditions and the following
\r
17 * disclaimer in the documentation and/or other materials
\r
18 * provided with the distribution.
\r
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
\r
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
\r
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
\r
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
\r
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
\r
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
\r
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
\r
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
\r
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
\r
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
\r
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
\r
32 * POSSIBILITY OF SUCH DAMAGE.
\r
34 * The views and conclusions contained in the software and
\r
35 * documentation are those of the authors and should not be
\r
36 * interpreted as representing official policies, either expressed
\r
37 * or implied, of GRNET S.A.
\r
39 * -----------------------------------------------------------------------
\r
42 using System.Collections.Specialized;
\r
43 using System.Diagnostics;
\r
44 using System.Diagnostics.Contracts;
\r
47 using System.Net.Http;
\r
48 using System.Reflection;
\r
49 using System.Runtime.Serialization;
\r
50 using System.Threading;
\r
51 using System.Threading.Tasks;
\r
55 namespace Pithos.Network
\r
58 using System.Collections.Generic;
\r
63 /// TODO: Update summary.
\r
65 public class RestHttpClient:HttpClient
\r
67 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
\r
69 //public bool TimedOut { get; set; }
\r
71 //public HttpStatusCode StatusCode { get; private set; }
\r
73 public string StatusDescription { get; set; }
\r
75 public long? RangeFrom { get; set; }
\r
76 public long? RangeTo { get; set; }
\r
78 public int Retries { get; set; }
\r
80 private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
\r
81 public Dictionary<string, string> Parameters
\r
85 Contract.Ensures(_parameters!=null);
\r
90 public RestHttpClient(HttpMessageHandler handler)
\r
94 public RestHttpClient():base(new HttpClientHandler
\r
96 AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
\r
101 //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
\r
102 //Any value above 2^21-1 will result in an empty response.
\r
103 //-1 essentially ignores the maximum length
\r
104 HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
\r
105 //this.MaxResponseContentBufferSize = -1;
\r
113 private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
\r
115 public List<HttpStatusCode> AllowedStatusCodes
\r
119 return _allowedStatusCodes;
\r
123 public DateTime LastModified { get; private set; }
\r
125 private static string LogContent(WebResponse webResponse)
\r
127 if (webResponse == null)
\r
128 throw new ArgumentNullException("webResponse");
\r
129 Contract.EndContractBlock();
\r
131 //The response stream must be copied to avoid affecting other code by disposing of the
\r
132 //original response stream.
\r
133 var stream = webResponse.GetResponseStream();
\r
134 using(var memStream=new MemoryStream())
\r
135 using (var reader = new StreamReader(memStream))
\r
137 stream.CopyTo(memStream);
\r
138 string content = reader.ReadToEnd();
\r
140 stream.Seek(0,SeekOrigin.Begin);
\r
146 public string GetStringWithRetry(Uri address, int retries = 0)
\r
148 if (address == null)
\r
149 throw new ArgumentNullException("address");
\r
151 var actualRetries = (retries == 0) ? Retries : retries;
\r
152 var task = GetAsync(address).WithRetries(Timeout,actualRetries)
\r
153 .ContinueWith(async r =>
\r
155 var response = r.Result;
\r
156 if (response.StatusCode == HttpStatusCode.NoContent)
\r
157 return String.Empty;
\r
158 if (response.StatusCode == HttpStatusCode.NotFound)
\r
160 return await response.Content.ReadAsStringAsync();
\r
163 var result = task.Result;
\r
169 public string DownloadStringWithRetry(string address,int retries=0)
\r
172 if (address == null)
\r
173 throw new ArgumentNullException("address");
\r
175 var actualAddress = GetActualAddress(address);
\r
177 TraceStart("GET",actualAddress);
\r
179 var actualRetries = (retries == 0) ? Retries : retries;
\r
182 var task = Retry(GetAsync(actualAddress)).ContinueWith(async () =>
\r
184 var response= await GetAsync(actualAddress);
\r
186 if (response.StatusCode== HttpStatusCode.NoContent)
\r
187 return String.Empty;
\r
188 return await response.Content.ReadAsStringAsync();
\r
194 var result = task.Result;
\r
198 catch (AggregateException exc)
\r
200 //If the task fails, propagate the original exception
\r
201 if (exc.InnerException!=null)
\r
202 throw exc.InnerException;
\r
208 /* public void Head(string address,int retries=0)
\r
210 AllowedStatusCodes.Add(HttpStatusCode.NotFound);
\r
211 RetryWithoutContent(address, retries, "HEAD");
\r
214 public void PutWithRetry(string address, int retries = 0, string contentType=null)
\r
216 RetryWithoutContent(address, retries, "PUT",contentType);
\r
219 public void PostWithRetry(string address,string contentType)
\r
221 RetryWithoutContent(address, 0, "POST",contentType);
\r
224 public void DeleteWithRetry(string address,int retries=0)
\r
226 RetryWithoutContent(address, retries, "DELETE");
\r
229 /* public string GetHeaderValue(string headerName,bool optional=false)
\r
231 if (this.ResponseHeaders==null)
\r
232 throw new InvalidOperationException("ResponseHeaders are null");
\r
233 Contract.EndContractBlock();
\r
235 var values=this.ResponseHeaders.GetValues(headerName);
\r
236 if (values != null)
\r
241 //A required header was not found
\r
242 throw new WebException(String.Format("The {0} header is missing", headerName));
\r
245 public void SetNonEmptyHeaderValue(string headerName, string value)
\r
247 if (String.IsNullOrWhiteSpace(value))
\r
249 DefaultRequestHeaders.Add(headerName,value);
\r
252 /* private void RetryWithoutContent(string address, int retries, string method,string contentType=null)
\r
254 if (address == null)
\r
255 throw new ArgumentNullException("address");
\r
257 var actualAddress = GetActualAddress(address);
\r
258 var actualRetries = (retries == 0) ? Retries : retries;
\r
260 var task = Retry(() =>
\r
262 var uriString = String.Join("/",BaseAddress ,actualAddress);
\r
263 var uri = new Uri(uriString);
\r
264 var request = GetWebRequest(uri);
\r
265 if (contentType!=null)
\r
267 request.ContentType = contentType;
\r
268 request.ContentLength = 0;
\r
270 request.Method = method;
\r
271 if (ResponseHeaders!=null)
\r
272 ResponseHeaders.Clear();
\r
274 TraceStart(method, uriString);
\r
275 if (method == "PUT")
\r
276 request.ContentLength = 0;
\r
278 //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
\r
279 //in order to return response headers
\r
280 var response = (HttpWebResponse)GetWebResponse(request);
\r
283 LastModified = response.LastModified;
\r
284 StatusCode = response.StatusCode;
\r
285 StatusDescription = response.StatusDescription;
\r
300 catch (AggregateException ex)
\r
302 var exc = ex.InnerException;
\r
303 if (exc is RetryException)
\r
305 Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
\r
309 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
\r
314 catch(Exception ex)
\r
316 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
\r
321 private static void TraceStart(string method, string actualAddress)
\r
323 Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
\r
326 private string GetActualAddress(string address)
\r
328 if (Parameters.Count == 0)
\r
330 var addressBuilder=new StringBuilder(address);
\r
332 bool isFirst = true;
\r
333 foreach (var parameter in Parameters)
\r
336 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
\r
338 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
\r
341 return addressBuilder.ToString();
\r
346 /*public void AssertStatusOK(string message)
\r
348 if (StatusCode >= HttpStatusCode.BadRequest)
\r
349 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
\r
355 private Task<T> Retry2<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
\r
357 if (original==null)
\r
358 throw new ArgumentNullException("original");
\r
359 Contract.EndContractBlock();
\r
362 tcs = new TaskCompletionSource<T>();
\r
363 Task.Factory.StartNew(original).ContinueWith(_original =>
\r
365 if (!_original.IsFaulted)
\r
366 tcs.SetFromTask(_original);
\r
369 var e = _original.Exception.InnerException;
\r
370 var we = (e as WebException);
\r
372 tcs.SetException(e);
\r
375 var statusCode = GetStatusCode(we);
\r
377 //Return null for 404
\r
378 if (statusCode == HttpStatusCode.NotFound)
\r
379 tcs.SetResult(default(T));
\r
380 //Retry for timeouts and service unavailable
\r
381 else if (we.Status == WebExceptionStatus.Timeout ||
\r
382 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
\r
385 if (retryCount == 0)
\r
387 Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
\r
388 tcs.SetException(new RetryException("Timed out too many times.", e));
\r
393 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
\r
395 Retry(original, retryCount - 1, tcs);
\r
399 tcs.SetException(e);
\r
407 /* private HttpStatusCode GetStatusCode(WebException we)
\r
410 throw new ArgumentNullException("we");
\r
411 var statusCode = HttpStatusCode.RequestTimeout;
\r
412 if (we.Response != null)
\r
414 statusCode = ((HttpWebResponse) we.Response).StatusCode;
\r
415 this.StatusCode = statusCode;
\r
420 public UriBuilder GetAddressBuilder(string container, string objectName)
\r
422 var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
\r
426 /* public Dictionary<string, string> GetMeta(string metaPrefix)
\r
428 if (String.IsNullOrWhiteSpace(metaPrefix))
\r
429 throw new ArgumentNullException("metaPrefix");
\r
430 Contract.EndContractBlock();
\r
432 var keys = ResponseHeaders.AllKeys.AsQueryable();
\r
433 var dict = (from key in keys
\r
434 where key.StartsWith(metaPrefix)
\r
435 let name = key.Substring(metaPrefix.Length)
\r
436 select new { Name = name, Value = ResponseHeaders[key] })
\r
437 .ToDictionary(t => t.Name, t => t.Value);
\r
442 internal Task DownloadFileTaskAsync(Uri uri, string fileName, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)
\r
444 cancellationToken.Register(CancelAsync);
\r
445 DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);
\r
446 this.DownloadProgressChanged += onDownloadProgressChanged;
\r
447 return this.DownloadFileTaskAsync(uri, fileName).ContinueWith(t=>
\r
449 this.DownloadProgressChanged -= onDownloadProgressChanged;
\r
453 internal Task<byte[]> DownloadDataTaskAsync(Uri uri, CancellationToken cancellationToken, IProgress<DownloadProgressChangedEventArgs> progress)
\r
455 cancellationToken.Register(CancelAsync);
\r
456 DownloadProgressChangedEventHandler onDownloadProgressChanged = (o, e) => progress.Report(e);
\r
457 this.DownloadProgressChanged += onDownloadProgressChanged;
\r
458 return this.DownloadDataTaskAsync(uri).ContinueWith(t =>
\r
460 this.DownloadProgressChanged -= onDownloadProgressChanged;
\r