2 /* -----------------------------------------------------------------------
3 * <copyright file="RestClient.cs" company="GRNet">
5 * Copyright 2011-2012 GRNET S.A. All rights reserved.
7 * Redistribution and use in source and binary forms, with or
8 * without modification, are permitted provided that the following
11 * 1. Redistributions of source code must retain the above
12 * copyright notice, this list of conditions and the following
15 * 2. Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials
18 * provided with the distribution.
21 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
22 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
25 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
28 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
34 * The views and conclusions contained in the software and
35 * documentation are those of the authors and should not be
36 * interpreted as representing official policies, either expressed
37 * or implied, of GRNET S.A.
39 * -----------------------------------------------------------------------
42 using System.Collections.Specialized;
43 using System.Diagnostics;
44 using System.Diagnostics.Contracts;
47 using System.Reflection;
48 using System.Runtime.Serialization;
49 using System.Threading.Tasks;
53 namespace Pithos.Network
56 using System.Collections.Generic;
61 /// TODO: Update summary.
63 public class RestClient:WebClient
65 private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
67 public int Timeout { get; set; }
69 public bool TimedOut { get; set; }
71 public HttpStatusCode StatusCode { get; private set; }
73 public string StatusDescription { get; set; }
75 public long? RangeFrom { get; set; }
76 public long? RangeTo { get; set; }
78 public int Retries { get; set; }
80 private readonly Dictionary<string, string> _parameters=new Dictionary<string, string>();
81 public Dictionary<string, string> Parameters
85 Contract.Ensures(_parameters!=null);
91 [ContractInvariantMethod]
92 private void Invariants()
94 Contract.Invariant(Headers!=null);
97 public RestClient():base()
99 //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
100 //Any value above 2^21-1 will result in an empty response.
101 //-1 essentially ignores the maximum length
102 HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
106 public RestClient(RestClient other)
110 //Log.ErrorFormat("[ERROR] No parameters provided to the rest client. \n{0}\n", other);
111 throw new ArgumentNullException("other");
112 Contract.EndContractBlock();
114 //The maximum error response must be large because missing server hashes are return as a Conflivt (409) error response
115 //Any value above 2^21-1 will result in an empty response.
116 //-1 essentially ignores the maximum length
117 HttpWebRequest.DefaultMaximumErrorResponseLength = -1;
120 Timeout = other.Timeout;
121 Retries = other.Retries;
122 BaseAddress = other.BaseAddress;
124 foreach (var parameter in other.Parameters)
126 Parameters.Add(parameter.Key,parameter.Value);
129 this.Proxy = other.Proxy;
133 protected override WebRequest GetWebRequest(Uri address)
136 var webRequest = base.GetWebRequest(address);
137 var request = (HttpWebRequest)webRequest;
138 request.ServicePoint.ConnectionLimit = 50;
139 if (IfModifiedSince.HasValue)
140 request.IfModifiedSince = IfModifiedSince.Value;
141 request.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip;
143 request.Timeout = Timeout;
145 if (RangeFrom.HasValue)
147 if (RangeTo.HasValue)
148 request.AddRange(RangeFrom.Value, RangeTo.Value);
150 request.AddRange(RangeFrom.Value);
155 public DateTime? IfModifiedSince { get; set; }
157 //Asynchronous version
158 protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result)
160 Log.InfoFormat("[{0}] {1}", request.Method, request.RequestUri);
161 HttpWebResponse response = null;
165 response = (HttpWebResponse)base.GetWebResponse(request, result);
167 catch (WebException exc)
169 if (!TryGetResponse(exc, request,out response))
173 StatusCode = response.StatusCode;
174 LastModified = response.LastModified;
175 StatusDescription = response.StatusDescription;
181 //Synchronous version
182 protected override WebResponse GetWebResponse(WebRequest request)
184 HttpWebResponse response = null;
187 Log.InfoFormat("[{0}] {1}",request.Method,request.RequestUri);
188 response = (HttpWebResponse)base.GetWebResponse(request);
190 catch (WebException exc)
192 if (!TryGetResponse(exc, request,out response))
196 StatusCode = response.StatusCode;
197 LastModified = response.LastModified;
198 StatusDescription = response.StatusDescription;
202 private bool TryGetResponse(WebException exc, WebRequest request,out HttpWebResponse response)
205 //Fail on empty response
206 if (exc.Response == null)
208 Log.WarnFormat("[{0}] {1} {2}", request.Method, exc.Status, request.RequestUri);
212 response = (exc.Response as HttpWebResponse);
213 var statusCode = (int)response.StatusCode;
214 //Succeed on allowed status codes
215 if (AllowedStatusCodes.Contains(response.StatusCode))
217 if (Log.IsDebugEnabled)
218 Log.DebugFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);
222 Log.WarnFormat("[{0}] {1} {2}", request.Method, statusCode, request.RequestUri);
224 //Does the response have any content to log?
225 if (exc.Response.ContentLength > 0)
227 var content = LogContent(exc.Response);
228 Log.ErrorFormat(content);
233 private readonly List<HttpStatusCode> _allowedStatusCodes=new List<HttpStatusCode>{HttpStatusCode.NotModified};
235 public List<HttpStatusCode> AllowedStatusCodes
239 return _allowedStatusCodes;
243 public DateTime LastModified { get; private set; }
245 private static string LogContent(WebResponse webResponse)
247 if (webResponse == null)
248 throw new ArgumentNullException("webResponse");
249 Contract.EndContractBlock();
251 //The response stream must be copied to avoid affecting other code by disposing of the
252 //original response stream.
253 var stream = webResponse.GetResponseStream();
254 using(var memStream=new MemoryStream())
255 using (var reader = new StreamReader(memStream))
257 stream.CopyTo(memStream);
258 string content = reader.ReadToEnd();
260 stream.Seek(0,SeekOrigin.Begin);
265 public string DownloadStringWithRetry(string address,int retries=0)
269 throw new ArgumentNullException("address");
271 var actualAddress = GetActualAddress(address);
273 TraceStart("GET",actualAddress);
275 var actualRetries = (retries == 0) ? Retries : retries;
277 var uriString = String.Join("/", BaseAddress.TrimEnd('/'), actualAddress);
279 var task = Retry(() =>
281 var content = base.DownloadString(uriString);
283 if (StatusCode == HttpStatusCode.NoContent)
291 var result = task.Result;
295 catch (AggregateException exc)
297 //If the task fails, propagate the original exception
298 if (exc.InnerException!=null)
299 throw exc.InnerException;
304 public void Head(string address,int retries=0)
306 AllowedStatusCodes.Add(HttpStatusCode.NotFound);
307 RetryWithoutContent(address, retries, "HEAD");
310 public void PutWithRetry(string address, int retries = 0, string contentType=null)
312 RetryWithoutContent(address, retries, "PUT",contentType);
315 public void PostWithRetry(string address,string contentType)
317 RetryWithoutContent(address, 0, "POST",contentType);
320 public void DeleteWithRetry(string address,int retries=0)
322 RetryWithoutContent(address, retries, "DELETE");
325 public string GetHeaderValue(string headerName,bool optional=false)
327 if (this.ResponseHeaders==null)
328 throw new InvalidOperationException("ResponseHeaders are null");
329 Contract.EndContractBlock();
331 var values=this.ResponseHeaders.GetValues(headerName);
337 //A required header was not found
338 throw new WebException(String.Format("The {0} header is missing", headerName));
341 public void SetNonEmptyHeaderValue(string headerName, string value)
343 if (String.IsNullOrWhiteSpace(value))
345 Headers.Add(headerName,value);
348 private void RetryWithoutContent(string address, int retries, string method,string contentType=null)
351 throw new ArgumentNullException("address");
353 var actualAddress = GetActualAddress(address);
354 var actualRetries = (retries == 0) ? Retries : retries;
356 var task = Retry(() =>
358 var uriString = String.Join("/",BaseAddress ,actualAddress);
359 var uri = new Uri(uriString);
360 var request = GetWebRequest(uri);
361 if (contentType!=null)
363 request.ContentType = contentType;
364 request.ContentLength = 0;
366 request.Method = method;
367 if (ResponseHeaders!=null)
368 ResponseHeaders.Clear();
370 TraceStart(method, uriString);
372 request.ContentLength = 0;
374 //Have to use try/finally instead of using here, because WebClient needs a valid WebResponse object
375 //in order to return response headers
376 var response = (HttpWebResponse)GetWebResponse(request);
379 LastModified = response.LastModified;
380 StatusCode = response.StatusCode;
381 StatusDescription = response.StatusDescription;
396 catch (AggregateException ex)
398 var exc = ex.InnerException;
399 if (exc is RetryException)
401 Log.ErrorFormat("[{0}] RETRY FAILED for {1} after {2} retries",method,address,retries);
405 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, exc);
412 Log.ErrorFormat("[{0}] FAILED for {1} with \n{2}", method, address, ex);
417 private static void TraceStart(string method, string actualAddress)
419 Log.InfoFormat("[{0}] {1} {2}", method, DateTime.Now, actualAddress);
422 private string GetActualAddress(string address)
424 if (Parameters.Count == 0)
426 var addressBuilder=new StringBuilder(address);
429 foreach (var parameter in Parameters)
432 addressBuilder.AppendFormat("?{0}={1}", parameter.Key, parameter.Value);
434 addressBuilder.AppendFormat("&{0}={1}", parameter.Key, parameter.Value);
437 return addressBuilder.ToString();
440 public string DownloadStringWithRetry(Uri address,int retries=0)
443 throw new ArgumentNullException("address");
445 var actualRetries = (retries == 0) ? Retries : retries;
446 var task = Retry(() =>
448 var content = base.DownloadString(address);
450 if (StatusCode == HttpStatusCode.NoContent)
456 var result = task.Result;
462 /// Copies headers from another RestClient
464 /// <param name="source">The RestClient from which the headers are copied</param>
465 public void CopyHeaders(RestClient source)
468 throw new ArgumentNullException("source", "source can't be null");
469 Contract.EndContractBlock();
470 //The Headers getter initializes the property, it is never null
471 Contract.Assume(Headers!=null);
473 CopyHeaders(source.Headers,Headers);
477 /// Copies headers from one header collection to another
479 /// <param name="source">The source collection from which the headers are copied</param>
480 /// <param name="target">The target collection to which the headers are copied</param>
481 public static void CopyHeaders(WebHeaderCollection source,WebHeaderCollection target)
484 throw new ArgumentNullException("source", "source can't be null");
486 throw new ArgumentNullException("target", "target can't be null");
487 Contract.EndContractBlock();
489 for (int i = 0; i < source.Count; i++)
491 target.Add(source.GetKey(i), source[i]);
495 public void AssertStatusOK(string message)
497 if (StatusCode >= HttpStatusCode.BadRequest)
498 throw new WebException(String.Format("{0} with code {1} - {2}", message, StatusCode, StatusDescription));
502 private Task<T> Retry<T>(Func<T> original, int retryCount, TaskCompletionSource<T> tcs = null)
505 throw new ArgumentNullException("original");
506 Contract.EndContractBlock();
509 tcs = new TaskCompletionSource<T>();
510 Task.Factory.StartNew(original).ContinueWith(_original =>
512 if (!_original.IsFaulted)
513 tcs.SetFromTask(_original);
516 var e = _original.Exception.InnerException;
517 var we = (e as WebException);
522 var statusCode = GetStatusCode(we);
524 //Return null for 404
525 if (statusCode == HttpStatusCode.NotFound)
526 tcs.SetResult(default(T));
527 //Retry for timeouts and service unavailable
528 else if (we.Status == WebExceptionStatus.Timeout ||
529 (we.Status == WebExceptionStatus.ProtocolError && statusCode == HttpStatusCode.ServiceUnavailable))
534 Log.ErrorFormat("[ERROR] Timed out too many times. \n{0}\n",e);
535 tcs.SetException(new RetryException("Timed out too many times.", e));
540 "[RETRY] Timed out after {0} ms. Will retry {1} more times\n{2}", Timeout,
542 Retry(original, retryCount - 1, tcs);
553 private HttpStatusCode GetStatusCode(WebException we)
556 throw new ArgumentNullException("we");
557 var statusCode = HttpStatusCode.RequestTimeout;
558 if (we.Response != null)
560 statusCode = ((HttpWebResponse) we.Response).StatusCode;
561 this.StatusCode = statusCode;
566 public UriBuilder GetAddressBuilder(string container, string objectName)
568 var builder = new UriBuilder(String.Join("/", BaseAddress, container, objectName));
572 public Dictionary<string, string> GetMeta(string metaPrefix)
574 if (String.IsNullOrWhiteSpace(metaPrefix))
575 throw new ArgumentNullException("metaPrefix");
576 Contract.EndContractBlock();
578 var keys = ResponseHeaders.AllKeys.AsQueryable();
579 var dict = (from key in keys
580 where key.StartsWith(metaPrefix)
581 let name = key.Substring(metaPrefix.Length)
582 select new { Name = name, Value = ResponseHeaders[key] })
583 .ToDictionary(t => t.Name, t => t.Value);
589 public class RetryException:Exception
591 public RetryException()
597 public RetryException(string message)
603 public RetryException(string message,Exception innerException)
604 :base(message,innerException)
609 public RetryException(SerializationInfo info,StreamingContext context)