// Copyright (c) Service Stack LLC. All Rights Reserved.
// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using ServiceStack.Logging;
using ServiceStack.Text;
using ServiceStack.Web;
#if NETFX_CORE
using Windows.System.Threading;
#endif
namespace ServiceStack
{
/**
* Need to provide async request options
* http://msdn.microsoft.com/en-us/library/86wf6409(VS.71).aspx
*/
public partial 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 GlobalRequestFilter { 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 GlobalResponseFilter { get; set; }
///
/// Called before request resend, when the initial request required authentication
///
public Action OnAuthenticationRequired { get; set; }
public static int BufferSize = 8192;
public ICredentials Credentials { get; set; }
public bool AlwaysSendBasicAuthHeader { get; set; }
public bool StoreCookies { get; set; }
public INameValueCollection Headers { 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 RequestFilter { 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 ResponseFilter { get; set; }
public string BaseUri { get; set; }
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; }
public string UserAgent { get; set; }
public bool CaptureSynchronizationContext { get; set; }
public bool HandleCallbackOnUiThread { get; set; }
public bool EmulateHttpViaPost { get; set; }
public ProgressDelegate OnDownloadProgress { get; set; }
public ProgressDelegate OnUploadProgress { get; set; }
public bool ShareCookiesWithBrowser { get; set; }
internal Action CancelAsyncFn;
public void CancelAsync()
{
if (CancelAsyncFn != 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
CancelAsyncFn();
CancelAsyncFn = null;
}
}
public Task SendAsync(string httpMethod, string absoluteUrl, object request)
{
var tcs = new TaskCompletionSource();
SendWebRequest(httpMethod, absoluteUrl, request,
tcs.SetResult,
(response, exc) => tcs.SetException(exc)
);
return tcs.Task;
}
public void SendAsync(string httpMethod, string absoluteUrl, object request,
Action onSuccess, Action onError)
{
SendWebRequest(httpMethod, absoluteUrl, request, onSuccess, onError);
}
private void SendWebRequest(string httpMethod, string absoluteUrl, object request,
Action onSuccess, Action onError)
{
if (httpMethod == null) throw new ArgumentNullException("httpMethod");
var requestUri = absoluteUrl;
var hasQueryString = request != null && !httpMethod.HasRequestBody();
if (hasQueryString)
{
var queryString = QueryStringSerializer.SerializeToString(request);
if (!string.IsNullOrEmpty(queryString))
{
requestUri += "?" + queryString;
}
}
var webRequest = this.CreateHttpWebRequest(requestUri);
var requestState = new AsyncState(BufferSize)
{
HttpMethod = httpMethod,
Url = requestUri,
WebRequest = webRequest,
Request = request,
OnSuccess = onSuccess,
OnError = onError,
UseSynchronizationContext = CaptureSynchronizationContext ? SynchronizationContext.Current : null,
HandleCallbackOnUIThread = HandleCallbackOnUiThread,
};
requestState.StartTimer(this.Timeout.GetValueOrDefault(DefaultTimeout));
SendWebRequestAsync(httpMethod, request, requestState, webRequest);
}
private void SendWebRequestAsync(string httpMethod, object request,
AsyncState state, HttpWebRequest webRequest)
{
webRequest.Accept = string.Format("{0}, */*", ContentType);
if (this.EmulateHttpViaPost)
{
webRequest.Method = "POST";
webRequest.Headers[HttpHeaders.XHttpMethodOverride] = httpMethod;
}
else
{
webRequest.Method = httpMethod;
}
PclExportClient.Instance.AddHeader(webRequest, Headers);
//EmulateHttpViaPost is also forced for SL5 clients sending non GET/POST requests
PclExport.Instance.Config(webRequest, userAgent: UserAgent);
if (this.Credentials != null)
webRequest.Credentials = this.Credentials;
if (this.AlwaysSendBasicAuthHeader)
webRequest.AddBasicAuth(this.UserName, this.Password);
ApplyWebRequestFilters(webRequest);
try
{
if (webRequest.Method.HasRequestBody())
{
webRequest.ContentType = ContentType;
webRequest.BeginGetRequestStream(RequestCallback, state);
}
else
{
state.WebRequest.BeginGetResponse(ResponseCallback, state);
}
}
catch (Exception ex)
{
// BeginGetRequestStream can throw if request was aborted
HandleResponseError(ex, state);
}
}
private void RequestCallback(IAsyncResult asyncResult)
{
var requestState = (AsyncState)asyncResult.AsyncState;
try
{
var req = requestState.WebRequest;
var stream = req.EndGetRequestStream(asyncResult);
if (requestState.Request != null)
{
StreamSerializer(null, requestState.Request, stream);
}
stream.EndWriteStream();
requestState.WebRequest.BeginGetResponse(ResponseCallback, requestState);
}
catch (Exception ex)
{
HandleResponseError(ex, requestState);
}
}
private void ResponseCallback(IAsyncResult asyncResult)
{
var requestState = (AsyncState)asyncResult.AsyncState;
try
{
var webRequest = requestState.WebRequest;
requestState.WebResponse = (HttpWebResponse)webRequest.EndGetResponse(asyncResult);
if (requestState.ResponseContentLength == default(long))
{
requestState.ResponseContentLength = requestState.WebResponse.ContentLength;
}
ApplyWebResponseFilters(requestState.WebResponse);
if (typeof(T) == typeof(HttpWebResponse))
{
requestState.HandleSuccess((T)(object)requestState.WebResponse);
return;
}
// Read the response into a Stream object.
var responseStream = requestState.WebResponse.GetResponseStream();
requestState.ResponseStream = responseStream;
var task = responseStream.ReadAsync(requestState.BufferRead, 0, BufferSize);
ReadCallBack(task, requestState);
}
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);
if (StoreCookies)
{
requestState.WebRequest.CookieContainer = CookieContainer;
}
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(Task task, AsyncState requestState)
{
task.ContinueWith(t =>
{
try
{
var responseStream = requestState.ResponseStream;
int read = t.Result;
if (read > 0)
{
requestState.BytesData.Write(requestState.BufferRead, 0, read);
var responeStreamTask = responseStream.ReadAsync(requestState.BufferRead, 0, BufferSize);
requestState.ResponseBytesRead += read;
if (OnDownloadProgress != null)
{
OnDownloadProgress(requestState.ResponseBytesRead, requestState.ResponseContentLength);
}
ReadCallBack(responeStreamTask, requestState);
return;
}
Interlocked.Increment(ref requestState.Completed);
var response = default(T);
try
{
requestState.BytesData.Position = 0;
if (typeof(T) == typeof(Stream))
{
response = (T)(object)requestState.BytesData;
}
else
{
var reader = requestState.BytesData;
try
{
if (typeof(T) == typeof(string))
{
using (var sr = new StreamReader(reader))
{
response = (T)(object)sr.ReadToEnd();
}
}
else if (typeof(T) == typeof(byte[]))
{
response = (T)(object)reader.ToArray();
}
else
{
response = (T)this.StreamDeserializer(typeof(T), reader);
}
}
finally
{
if (reader.CanRead)
reader.Dispose(); // Not yet disposed, but could've been.
}
}
PclExportClient.Instance.SynchronizeCookies(this);
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.EndReadStream();
CancelAsyncFn = null;
}
}
catch (Exception ex)
{
HandleResponseError(ex, requestState);
}
});
}
private void HandleResponseError(Exception exception, AsyncState state)
{
var webEx = exception as WebException;
if (webEx.IsWebException())
{
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,
StatusDescription = errorResponse.StatusDescription,
};
try
{
using (var stream = errorResponse.GetResponseStream())
{
var bytes = stream.ReadFully();
serviceEx.ResponseBody = bytes.FromUtf8Bytes();
if (stream.CanSeek)
{
PclExport.Instance.ResetStream(stream);
serviceEx.ResponseDto = this.StreamDeserializer(typeof(TResponse), stream);
}
else //Android
{
using (var ms = MemoryStreamFactory.GetStream(bytes))
{
serviceEx.ResponseDto = this.StreamDeserializer(typeof(TResponse), ms);
}
}
state.HandleError((TResponse)serviceEx.ResponseDto, serviceEx);
}
}
catch (Exception innerEx)
{
// Oh, well, we tried
Log.Debug(string.Format("WebException Reading Response Error: {0}", innerEx.Message), innerEx);
state.HandleError(default(TResponse), new WebServiceException(errorResponse.StatusDescription, innerEx) {
StatusCode = (int)errorResponse.StatusCode,
StatusDescription = errorResponse.StatusDescription,
});
}
return;
}
var authEx = exception as AuthenticationException;
if (authEx != null)
{
var customEx = WebRequestUtils.CreateCustomException(state.Url, authEx);
Log.Debug(string.Format("AuthenticationException: {0}", customEx.Message), customEx);
state.HandleError(default(TResponse), authEx);
}
Log.Debug(string.Format("Exception Reading Response Error: {0}", exception.Message), exception);
state.HandleError(default(TResponse), exception);
CancelAsyncFn = null;
}
private void ApplyWebResponseFilters(WebResponse webResponse)
{
if (!(webResponse is HttpWebResponse)) return;
if (ResponseFilter != null)
ResponseFilter((HttpWebResponse)webResponse);
if (GlobalResponseFilter != null)
GlobalResponseFilter((HttpWebResponse)webResponse);
}
private void ApplyWebRequestFilters(HttpWebRequest client)
{
if (RequestFilter != null)
RequestFilter(client);
if (GlobalRequestFilter != null)
GlobalRequestFilter(client);
}
public void Dispose() { }
}
}