using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using ServiceStack.Logging;
using ServiceStack.ServiceHost;
using ServiceStack.Text;
using ServiceStack.Common.Web;
namespace ServiceStack.ServiceClient.Web
{
/**
* Need to provide async request options
* http://msdn.microsoft.com/en-us/library/86wf6409(VS.71).aspx
*/
public class AsyncServiceClient
{
private static readonly ILog Log = LogManager.GetLogger(typeof(AsyncServiceClient));
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
private HttpWebRequest _webRequest = null;
///
/// The request filter is called before any request.
/// This request filter is executed globally.
///
public static Action HttpWebRequestFilter { get; set; }
///
/// The response action is called once the server response is available.
/// It will allow you to access raw response information.
/// This response action is executed globally.
/// Note that you should NOT consume the response stream as this is handled by ServiceStack
///
public static Action HttpWebResponseFilter { get; set; }
///
/// Called before request resend, when the initial request required authentication
///
public Action OnAuthenticationRequired { get; set; }
const int BufferSize = 4096;
public ICredentials Credentials { get; set; }
public bool StoreCookies { get; set; }
public CookieContainer CookieContainer { get; set; }
///
/// The request filter is called before any request.
/// This request filter only works with the instance where it was set (not global).
///
public Action LocalHttpWebRequestFilter { get; set; }
///
/// The response action is called once the server response is available.
/// It will allow you to access raw response information.
/// Note that you should NOT consume the response stream as this is handled by ServiceStack
///
public Action LocalHttpWebResponseFilter { get; set; }
public string BaseUri { get; set; }
internal class RequestState : IDisposable
{
private bool _timedOut; // Pass the correct error back even on Async Calls
public RequestState()
{
BufferRead = new byte[BufferSize];
TextData = new StringBuilder();
BytesData = new MemoryStream(BufferSize);
WebRequest = null;
ResponseStream = null;
}
public string HttpMethod;
public string Url;
public StringBuilder TextData;
public MemoryStream BytesData;
public byte[] BufferRead;
public object Request;
public HttpWebRequest WebRequest;
public HttpWebResponse WebResponse;
public Stream ResponseStream;
public int Completed;
public int RequestCount;
public Timer Timer;
public Action OnSuccess;
public Action OnError;
#if SILVERLIGHT
public bool HandleCallbackOnUIThread { get; set; }
#endif
public void HandleSuccess(TResponse response)
{
if (this.OnSuccess == null)
return;
#if SILVERLIGHT
if (this.HandleCallbackOnUIThread)
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnSuccess(response));
else
this.OnSuccess(response);
#else
this.OnSuccess(response);
#endif
}
public void HandleError(TResponse response, Exception ex)
{
if (this.OnError == null)
return;
Exception toReturn = ex;
if (_timedOut)
{
#if SILVERLIGHT
WebException we = new WebException("The request timed out", ex, WebExceptionStatus.RequestCanceled, null);
#else
WebException we = new WebException("The request timed out", ex, WebExceptionStatus.Timeout, null);
#endif
toReturn = we;
}
#if SILVERLIGHT
if (this.HandleCallbackOnUIThread)
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() => this.OnError(response, toReturn));
else
this.OnError(response, toReturn);
#else
OnError(response, toReturn);
#endif
}
public void StartTimer(TimeSpan timeOut)
{
this.Timer = new Timer(this.TimedOut, this, (int)timeOut.TotalMilliseconds, System.Threading.Timeout.Infinite);
}
public void TimedOut(object state)
{
if (Interlocked.Increment(ref Completed) == 1)
{
if (this.WebRequest != null)
{
_timedOut = true;
this.WebRequest.Abort();
}
}
this.Timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
this.Timer.Dispose();
this.Dispose();
}
public void Dispose()
{
if (this.BytesData == null) return;
this.BytesData.Dispose();
this.BytesData = null;
}
}
public bool DisableAutoCompression { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public void SetCredentials(string userName, string password)
{
this.UserName = userName;
this.Password = password;
}
public TimeSpan? Timeout { get; set; }
public string ContentType { get; set; }
public StreamSerializerDelegate StreamSerializer { get; set; }
public StreamDeserializerDelegate StreamDeserializer { get; set; }
#if SILVERLIGHT
public bool HandleCallbackOnUIThread { get; set; }
public bool UseBrowserHttpHandling { get; set; }
public bool ShareCookiesWithBrowser { get; set; }
#endif
public void SendAsync(string httpMethod, string absoluteUrl, object request,
Action onSuccess, Action onError)
{
SendWebRequest(httpMethod, absoluteUrl, request, onSuccess, onError);
}
public void CancelAsync()
{
if (_webRequest != null)
{
// Request will be nulled after it throws an exception on its async methods
// See - http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.abort
_webRequest.Abort();
}
}
#if !SILVERLIGHT
internal static void AllowAutoCompression(HttpWebRequest webRequest)
{
webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
#endif
private RequestState SendWebRequest(string httpMethod, string absoluteUrl, object request,
Action onSuccess, Action onError)
{
if (httpMethod == null) throw new ArgumentNullException("httpMethod");
var requestUri = absoluteUrl;
var httpGetOrDelete = (httpMethod == "GET" || httpMethod == "DELETE");
var hasQueryString = request != null && httpGetOrDelete;
if (hasQueryString)
{
var queryString = QueryStringSerializer.SerializeToString(request);
if (!string.IsNullOrEmpty(queryString))
{
requestUri += "?" + queryString;
}
}
#if SILVERLIGHT
var creator = this.UseBrowserHttpHandling
? System.Net.Browser.WebRequestCreator.BrowserHttp
: System.Net.Browser.WebRequestCreator.ClientHttp;
var webRequest = (HttpWebRequest) creator.Create(new Uri(requestUri));
if (StoreCookies && !UseBrowserHttpHandling)
{
if (ShareCookiesWithBrowser)
{
if (CookieContainer == null)
CookieContainer = new CookieContainer();
CookieContainer.SetCookies(new Uri(BaseUri), System.Windows.Browser.HtmlPage.Document.Cookies);
}
webRequest.CookieContainer = CookieContainer;
}
#else
_webRequest = (HttpWebRequest)WebRequest.Create(requestUri);
if (StoreCookies)
{
_webRequest.CookieContainer = CookieContainer;
}
#endif
#if !SILVERLIGHT
if (!DisableAutoCompression)
{
_webRequest.Headers.Add(HttpRequestHeader.AcceptEncoding, "gzip,deflate");
_webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
#endif
var requestState = new RequestState
{
HttpMethod = httpMethod,
Url = requestUri,
#if SILVERLIGHT
WebRequest = webRequest,
#else
WebRequest = _webRequest,
#endif
Request = request,
OnSuccess = onSuccess,
OnError = onError,
#if SILVERLIGHT
HandleCallbackOnUIThread = HandleCallbackOnUIThread,
#endif
};
requestState.StartTimer(this.Timeout.GetValueOrDefault(DefaultTimeout));
#if SILVERLIGHT
SendWebRequestAsync(httpMethod, request, requestState, webRequest);
#else
SendWebRequestAsync(httpMethod, request, requestState, _webRequest);
#endif
return requestState;
}
private void SendWebRequestAsync(string httpMethod, object request,
RequestState requestState, HttpWebRequest webRequest)
{
var httpGetOrDelete = (httpMethod == "GET" || httpMethod == "DELETE");
webRequest.Accept = string.Format("{0}, */*", ContentType);
#if !SILVERLIGHT
webRequest.Method = httpMethod;
#else
//Methods others than GET and POST are only supported by Client request creator, see
//http://msdn.microsoft.com/en-us/library/cc838250(v=vs.95).aspx
if (this.UseBrowserHttpHandling && httpMethod != "GET" && httpMethod != "POST")
{
webRequest.Method = "POST";
webRequest.Headers[HttpHeaders.XHttpMethodOverride] = httpMethod;
}
else
{
webRequest.Method = httpMethod;
}
#endif
if (this.Credentials != null)
{
webRequest.Credentials = this.Credentials;
}
ApplyWebRequestFilters(webRequest);
try
{
if (!httpGetOrDelete && request != null)
{
webRequest.ContentType = ContentType;
webRequest.BeginGetRequestStream(RequestCallback, requestState);
}
else
{
requestState.WebRequest.BeginGetResponse(ResponseCallback, requestState);
}
}
catch (Exception ex)
{
// BeginGetRequestStream can throw if request was aborted
HandleResponseError(ex, requestState);
}
}
private void RequestCallback(IAsyncResult asyncResult)
{
var requestState = (RequestState)asyncResult.AsyncState;
try
{
var req = requestState.WebRequest;
var postStream = req.EndGetRequestStream(asyncResult);
StreamSerializer(null, requestState.Request, postStream);
postStream.Close();
requestState.WebRequest.BeginGetResponse(ResponseCallback, requestState);
}
catch (Exception ex)
{
HandleResponseError(ex, requestState);
}
}
private void ResponseCallback(IAsyncResult asyncResult)
{
var requestState = (RequestState)asyncResult.AsyncState;
try
{
var webRequest = requestState.WebRequest;
requestState.WebResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult);
ApplyWebResponseFilters(requestState.WebResponse);
// Read the response into a Stream object.
var responseStream = requestState.WebResponse.GetResponseStream();
requestState.ResponseStream = responseStream;
responseStream.BeginRead(requestState.BufferRead, 0, BufferSize, ReadCallBack, requestState);
return;
}
catch (Exception ex)
{
var firstCall = Interlocked.Increment(ref requestState.RequestCount) == 1;
if (firstCall && WebRequestUtils.ShouldAuthenticate(ex, this.UserName, this.Password))
{
try
{
requestState.WebRequest = (HttpWebRequest)WebRequest.Create(requestState.Url);
requestState.WebRequest.AddBasicAuth(this.UserName, this.Password);
if (OnAuthenticationRequired != null)
{
OnAuthenticationRequired(requestState.WebRequest);
}
SendWebRequestAsync(
requestState.HttpMethod, requestState.Request,
requestState, requestState.WebRequest);
}
catch (Exception /*subEx*/)
{
HandleResponseError(ex, requestState);
}
return;
}
HandleResponseError(ex, requestState);
}
}
private void ReadCallBack(IAsyncResult asyncResult)
{
var requestState = (RequestState)asyncResult.AsyncState;
try
{
var responseStream = requestState.ResponseStream;
int read = responseStream.EndRead(asyncResult);
if (read > 0)
{
requestState.BytesData.Write(requestState.BufferRead, 0, read);
responseStream.BeginRead(
requestState.BufferRead, 0, BufferSize, ReadCallBack, requestState);
return;
}
Interlocked.Increment(ref requestState.Completed);
var response = default(T);
try
{
requestState.BytesData.Position = 0;
using (var reader = requestState.BytesData)
{
response = (T)this.StreamDeserializer(typeof(T), reader);
}
#if SILVERLIGHT
if (this.StoreCookies && this.ShareCookiesWithBrowser && !this.UseBrowserHttpHandling)
{
// browser cookies must be set on the ui thread
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(
() =>
{
var cookieHeader = this.CookieContainer.GetCookieHeader(new Uri(BaseUri));
System.Windows.Browser.HtmlPage.Document.Cookies = cookieHeader;
});
}
#endif
requestState.HandleSuccess(response);
}
catch (Exception ex)
{
Log.Debug(string.Format("Error Reading Response Error: {0}", ex.Message), ex);
requestState.HandleError(default(T), ex);
}
finally
{
responseStream.Close();
_webRequest = null;
}
}
catch (Exception ex)
{
HandleResponseError(ex, requestState);
}
}
private void HandleResponseError(Exception exception, RequestState requestState)
{
var webEx = exception as WebException;
if (webEx != null
#if !SILVERLIGHT
&& webEx.Status == WebExceptionStatus.ProtocolError
#endif
)
{
var errorResponse = ((HttpWebResponse)webEx.Response);
Log.Error(webEx);
Log.DebugFormat("Status Code : {0}", errorResponse.StatusCode);
Log.DebugFormat("Status Description : {0}", errorResponse.StatusDescription);
var serviceEx = new WebServiceException(errorResponse.StatusDescription)
{
StatusCode = (int)errorResponse.StatusCode,
};
try
{
using (var stream = errorResponse.GetResponseStream())
{
//Uncomment to Debug exceptions:
//var strResponse = new StreamReader(stream).ReadToEnd();
//Console.WriteLine("Response: " + strResponse);
//stream.Position = 0;
serviceEx.ResponseDto = this.StreamDeserializer(typeof(TResponse), stream);
requestState.HandleError((TResponse)serviceEx.ResponseDto, serviceEx);
}
}
catch (Exception innerEx)
{
// Oh, well, we tried
Log.Debug(string.Format("WebException Reading Response Error: {0}", innerEx.Message), innerEx);
requestState.HandleError(default(TResponse), new WebServiceException(errorResponse.StatusDescription, innerEx)
{
StatusCode = (int)errorResponse.StatusCode,
});
}
return;
}
var authEx = exception as AuthenticationException;
if (authEx != null)
{
var customEx = WebRequestUtils.CreateCustomException(requestState.Url, authEx);
Log.Debug(string.Format("AuthenticationException: {0}", customEx.Message), customEx);
requestState.HandleError(default(TResponse), authEx);
}
Log.Debug(string.Format("Exception Reading Response Error: {0}", exception.Message), exception);
requestState.HandleError(default(TResponse), exception);
_webRequest = null;
}
private void ApplyWebResponseFilters(WebResponse webResponse)
{
if (!(webResponse is HttpWebResponse)) return;
if (HttpWebResponseFilter != null)
HttpWebResponseFilter((HttpWebResponse)webResponse);
if (LocalHttpWebResponseFilter != null)
LocalHttpWebResponseFilter((HttpWebResponse)webResponse);
}
private void ApplyWebRequestFilters(HttpWebRequest client)
{
if (LocalHttpWebRequestFilter != null)
LocalHttpWebRequestFilter(client);
if (HttpWebRequestFilter != null)
HttpWebRequestFilter(client);
}
public void Dispose() { }
}
}