Skip to content
Browse files

Add better support for end-to-end Error Handling.

Now you don't have to return a typed Response DTO to get error handling - if there is no Response DTO it uses the Generic ErrorResponse DTO to capture errors.
  • Loading branch information...
1 parent bb0335e commit 1874900c39aa3c8c51bcaa0c4c0066aa540062bc @mythz mythz committed
View
77 src/ServiceStack.Common/ServiceClient.Web/ServiceClientBase.cs
@@ -3,11 +3,13 @@
using System.IO;
using System.Net;
#if !(MONOTOUCH || SILVERLIGHT)
+using System.Reflection;
using System.Web;
#endif
using ServiceStack.Common;
using ServiceStack.Common.Web;
using ServiceStack.Logging;
+using ServiceStack.Net30.Collections.Concurrent;
using ServiceStack.Service;
using ServiceStack.ServiceHost;
using ServiceStack.Text;
@@ -74,8 +76,7 @@ protected ServiceClientBase()
{
this.HttpMethod = DefaultHttpMethod;
this.CookieContainer = new CookieContainer();
- asyncClient = new AsyncServiceClient
- {
+ asyncClient = new AsyncServiceClient {
ContentType = ContentType,
StreamSerializer = SerializeToStream,
StreamDeserializer = StreamDeserializer,
@@ -350,7 +351,12 @@ public virtual TResponse Send<TResponse>(object request)
{
TResponse response;
- if (!HandleResponseException(ex, requestUri, () => SendRequest(Web.HttpMethod.Post, requestUri, request), c => c.GetResponse(), out response))
+ if (!HandleResponseException(ex,
+ request,
+ requestUri,
+ () => SendRequest(Web.HttpMethod.Post, requestUri, request),
+ c => c.GetResponse(),
+ out response))
{
throw;
}
@@ -359,7 +365,8 @@ public virtual TResponse Send<TResponse>(object request)
}
}
- private bool HandleResponseException<TResponse>(Exception ex, string requestUri, Func<WebRequest> createWebRequest, Func<WebRequest, WebResponse> getResponse, out TResponse response)
+ private bool HandleResponseException<TResponse>(Exception ex, object request, string requestUri,
+ Func<WebRequest> createWebRequest, Func<WebRequest, WebResponse> getResponse, out TResponse response)
{
try
{
@@ -385,20 +392,47 @@ private bool HandleResponseException<TResponse>(Exception ex, string requestUri,
// than the old one.
// The new exception is either this one or the one thrown
// by the following method.
- HandleResponseException<TResponse>(subEx, requestUri);
+ ThrowResponseTypeException<TResponse>(request, subEx, requestUri);
throw;
}
// If this doesn't throw, the calling method
// should rethrow the original exception upon
// return value of false.
- HandleResponseException<TResponse>(ex, requestUri);
+ ThrowResponseTypeException<TResponse>(request, ex, requestUri);
response = default(TResponse);
return false;
}
- private void HandleResponseException<TResponse>(Exception ex, string requestUri)
+ readonly ConcurrentDictionary<Type,Action<Exception,string>> ResponseHandlers
+ = new ConcurrentDictionary<Type, Action<Exception, string>>();
+
+ private void ThrowResponseTypeException<TResponse>(object request, Exception ex, string requestUri)
+ {
+ if (request == null)
+ {
+ ThrowWebServiceException<TResponse>(ex, requestUri);
+ return;
+ }
+
+ var responseType = WebRequestUtils.GetErrorResponseDtoType(request);
+ Action<Exception, string> responseHandler;
+ if (!ResponseHandlers.TryGetValue(responseType, out responseHandler))
+ {
+ var mi = GetType().GetMethod("ThrowWebServiceException",
+ BindingFlags.Instance | BindingFlags.NonPublic)
+ .MakeGenericMethod(new[] { responseType });
+
+ responseHandler = (Action<Exception, string>)Delegate.CreateDelegate(
+ typeof(Action<Exception, string>), this, mi);
+
+ ResponseHandlers[responseType] = responseHandler;
+ }
+ responseHandler(ex, requestUri);
+ }
+
+ internal void ThrowWebServiceException<TResponse>(Exception ex, string requestUri)
{
var webEx = ex as WebException;
if (webEx != null && webEx.Status == WebExceptionStatus.ProtocolError)
@@ -408,8 +442,7 @@ private void HandleResponseException<TResponse>(Exception ex, string requestUri)
log.DebugFormat("Status Code : {0}", errorResponse.StatusCode);
log.DebugFormat("Status Description : {0}", errorResponse.StatusDescription);
- var serviceEx = new WebServiceException(errorResponse.StatusDescription)
- {
+ var serviceEx = new WebServiceException(errorResponse.StatusDescription) {
StatusCode = (int)errorResponse.StatusCode,
StatusDescription = errorResponse.StatusDescription,
};
@@ -433,8 +466,7 @@ private void HandleResponseException<TResponse>(Exception ex, string requestUri)
catch (Exception innerEx)
{
// Oh, well, we tried
- throw new WebServiceException(errorResponse.StatusDescription, innerEx)
- {
+ throw new WebServiceException(errorResponse.StatusDescription, innerEx) {
StatusCode = (int)errorResponse.StatusCode,
StatusDescription = errorResponse.StatusDescription,
ResponseBody = serviceEx.ResponseBody
@@ -459,8 +491,7 @@ private WebRequest SendRequest(string requestUri, object request)
private WebRequest SendRequest(string httpMethod, string requestUri, object request)
{
- return PrepareWebRequest(httpMethod, requestUri, request, client =>
- {
+ return PrepareWebRequest(httpMethod, requestUri, request, client => {
using (var requestStream = client.GetRequestStream())
{
SerializeToStream(null, request, requestStream);
@@ -736,7 +767,13 @@ public virtual TResponse Send<TResponse>(string httpMethod, string relativeOrAbs
{
TResponse response;
- if (!HandleResponseException(ex, requestUri, () => SendRequest(httpMethod, requestUri, request), c => c.GetResponse(), out response))
+ if (!HandleResponseException(
+ ex,
+ request,
+ requestUri,
+ () => SendRequest(httpMethod, requestUri, request),
+ c => c.GetResponse(),
+ out response))
{
throw;
}
@@ -815,7 +852,7 @@ public virtual void Patch(IReturnVoid request)
SendOneWay(Web.HttpMethod.Patch, request.ToUrl(Web.HttpMethod.Patch), request);
}
- public virtual TResponse Patch<TResponse>(string relativeOrAbsoluteUrl, object request)
+ public virtual TResponse Patch<TResponse>(string relativeOrAbsoluteUrl, object request)
{
return Send<TResponse>(Web.HttpMethod.Patch, relativeOrAbsoluteUrl, request);
}
@@ -895,7 +932,8 @@ public virtual TResponse PostFileWithRequest<TResponse>(string relativeOrAbsolut
// restore original position before retry
fileToUpload.Seek(currentStreamPosition, SeekOrigin.Begin);
- if (!HandleResponseException(ex, requestUri, createWebRequest, c => c.GetResponse(), out response))
+ if (!HandleResponseException(
+ ex, request, requestUri, createWebRequest, c => c.GetResponse(), out response))
{
throw;
}
@@ -929,7 +967,12 @@ public virtual TResponse PostFile<TResponse>(string relativeOrAbsoluteUrl, Strea
// restore original position before retry
fileToUpload.Seek(currentStreamPosition, SeekOrigin.Begin);
- if (!HandleResponseException(ex, requestUri, createWebRequest, c => { c.UploadFile(fileToUpload, fileName, mimeType); return c.GetResponse(); }, out response))
+ if (!HandleResponseException(ex,
+ null,
+ requestUri,
+ createWebRequest,
+ c => { c.UploadFile(fileToUpload, fileName, mimeType); return c.GetResponse(); },
+ out response))
{
throw;
}
View
48 src/ServiceStack.Common/ServiceClient.Web/WebRequestUtils.cs
@@ -2,6 +2,9 @@
using System.Net;
using System.Text;
using ServiceStack.Common.Web;
+using ServiceStack.ServiceHost;
+using ServiceStack.ServiceInterface.ServiceModel;
+using ServiceStack.Text;
namespace ServiceStack.ServiceClient.Web
{
@@ -20,14 +23,14 @@ public AuthenticationException(string message, Exception innerException) : base(
}
}
- internal static class WebRequestUtils
+ public static class WebRequestUtils
{
internal static AuthenticationException CreateCustomException(string uri, AuthenticationException ex)
{
if (uri.StartsWith("https"))
{
return new AuthenticationException(
- string.Format("Invalid remote SSL certificate, overide with: \nServicePointManager.ServerCertificateValidationCallback += ((sender, certificate, chain, sslPolicyErrors) => isValidPolicy);"),
+ String.Format("Invalid remote SSL certificate, overide with: \nServicePointManager.ServerCertificateValidationCallback += ((sender, certificate, chain, sslPolicyErrors) => isValidPolicy);"),
ex);
}
return null;
@@ -39,8 +42,8 @@ internal static bool ShouldAuthenticate(Exception ex, string userName, string pa
return (webEx != null
&& webEx.Response != null
&& ((HttpWebResponse) webEx.Response).StatusCode == HttpStatusCode.Unauthorized
- && !string.IsNullOrEmpty(userName)
- && !string.IsNullOrEmpty(password));
+ && !String.IsNullOrEmpty(userName)
+ && !String.IsNullOrEmpty(password));
}
internal static void AddBasicAuth(this WebRequest client, string userName, string password)
@@ -49,6 +52,43 @@ internal static void AddBasicAuth(this WebRequest client, string userName, strin
= "basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(userName + ":" + password));
}
+ /// <summary>
+ /// Naming convention for the request's Response DTO
+ /// </summary>
+ public const string ResponseDtoSuffix = "Response";
+
+ public static string GetResponseDtoName<TRequest>(TRequest request)
+ {
+ return typeof(TRequest) != typeof(object)
+ ? typeof(TRequest).FullName + ResponseDtoSuffix
+ : request.GetType().FullName + ResponseDtoSuffix;
+ }
+
+ public static Type GetErrorResponseDtoType(object request)
+ {
+ //If a conventionally-named Response type exists use that regardless if it has ResponseStatus or not
+ var responseDtoType = AssemblyUtils.FindType(GetResponseDtoName(request));
+ if (responseDtoType == null)
+ {
+ var genericDef = request.GetType().GetTypeWithGenericTypeDefinitionOf(typeof(IReturn<>));
+ if (genericDef != null)
+ {
+
+ var returnDtoType = genericDef.GetGenericArguments()[0];
+ var hasResponseStatus = returnDtoType is IHasResponseStatus
+ || returnDtoType.GetProperty("ResponseStatus") != null;
+
+ //Only use the specified Return type if it has a ResponseStatus property
+ if (hasResponseStatus)
+ {
+ responseDtoType = returnDtoType;
+ }
+ }
+ }
+
+ return responseDtoType ?? typeof(ErrorResponse);
+ }
+
}
}
View
14 src/ServiceStack.Common/ServiceClient.Web/XmlServiceClient.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Xml;
using ServiceStack.ServiceHost;
using ServiceStack.ServiceModel.Serialization;
using ServiceStack.Text;
@@ -33,12 +34,23 @@ public override string ContentType
public override void SerializeToStream(IRequestContext requestContext, object request, Stream stream)
{
+ if (request == null) return;
DataContractSerializer.Instance.SerializeToStream(request, stream);
}
public override T DeserializeFromStream<T>(Stream stream)
{
- return DataContractDeserializer.Instance.DeserializeFromStream<T>(stream);
+ try
+ {
+ return DataContractDeserializer.Instance.DeserializeFromStream<T>(stream);
+ }
+ catch (XmlException ex)
+ {
+ if (ex.Message == "Unexpected end of file.") //Empty responses
+ return default(T);
+
+ throw;
+ }
}
public override StreamDeserializerDelegate StreamDeserializer
View
2 src/ServiceStack.Interfaces/ServiceInterface.ServiceModel/ErrorResponse.cs
@@ -8,7 +8,7 @@ namespace ServiceStack.ServiceInterface.ServiceModel
/// </summary>
[DataContract]
- public class ErrorResponse
+ public class ErrorResponse : IHasResponseStatus
{
[DataMember]
public ResponseStatus ResponseStatus { get; set; }
View
4 src/ServiceStack.ServiceInterface/AsyncServiceBase.cs
@@ -1,6 +1,8 @@
using System;
using ServiceStack.Messaging;
+using ServiceStack.ServiceClient.Web;
using ServiceStack.ServiceHost;
+using ServiceStack.Text;
namespace ServiceStack.ServiceInterface
{
@@ -37,7 +39,7 @@ public override object ExecuteAsync(TRequest request)
producer.Publish(request);
}
- return DtoUtils.CreateResponseDto(request);
+ return WebRequestUtils.GetErrorResponseDtoType(request).CreateInstance();
}
/// <summary>
View
108 src/ServiceStack.ServiceInterface/ErrorHandler.cs
@@ -1,108 +0,0 @@
-using System;
-using ServiceStack.Common;
-using ServiceStack.Logging;
-using ServiceStack.Redis;
-using ServiceStack.ServiceHost;
-using ServiceStack.ServiceInterface.ServiceModel;
-using ServiceStack.Text;
-using ServiceStack.WebHost.Endpoints;
-
-namespace ServiceStack.ServiceInterface
-{
- public interface IErrorHandler
- {
- object HandleException<TRequest>(IAppHost appHost, TRequest request, Exception ex);
- }
-
- public class ErrorHandler : IErrorHandler
- {
- private static readonly ILog Log = LogManager.GetLogger(typeof(ErrorHandler));
-
- public static IErrorHandler Instance = new ErrorHandler();
-
- /// <summary>
- /// Service error logs are kept in 'urn:ServiceErrors:{ServiceName}'
- /// </summary>
- public const string UrnServiceErrorType = "ServiceErrors";
-
- /// <summary>
- /// Combined service error logs are maintained in 'urn:ServiceErrors:All'
- /// </summary>
- public const string CombinedServiceLogId = "All";
-
- public object HandleException<TRequest>(IAppHost appHost, TRequest request, Exception ex)
- {
- if (ex.InnerException != null && !(ex is IHttpError))
- ex = ex.InnerException;
-
- var responseStatus = ResponseStatusTranslator.Instance.Parse(ex);
-
- if (EndpointHost.UserConfig.DebugMode)
- {
- // View stack trace in tests and on the client
- responseStatus.StackTrace = GetRequestErrorBody(request) + ex;
- }
-
- Log.Error("ServiceBase<TRequest>::Service Exception", ex);
-
- if (appHost != null)
- {
- //If Redis is configured, maintain rolling service error logs in Redis (an in-memory datastore)
- var redisManager = appHost.TryResolve<IRedisClientsManager>();
- if (redisManager != null)
- {
- try
- {
- //Get a thread-safe redis client from the client manager pool
- using (var client = redisManager.GetClient())
- {
- //Get a client with a native interface for storing 'ResponseStatus' objects
- var redis = client.GetTypedClient<ResponseStatus>();
-
- //Store the errors in predictable Redis-named lists i.e.
- //'urn:ServiceErrors:{ServiceName}' and 'urn:ServiceErrors:All'
- var redisSeriviceErrorList = redis.Lists[UrnId.Create(UrnServiceErrorType, typeof(TRequest).Name)];
- var redisCombinedErrorList = redis.Lists[UrnId.Create(UrnServiceErrorType, CombinedServiceLogId)];
-
- //Append the error at the start of the service-specific and combined error logs.
- redisSeriviceErrorList.Prepend(responseStatus);
- redisCombinedErrorList.Prepend(responseStatus);
-
- //Clip old error logs from the managed logs
- const int rollingErrorCount = 1000;
- redisSeriviceErrorList.Trim(0, rollingErrorCount);
- redisCombinedErrorList.Trim(0, rollingErrorCount);
- }
- }
- catch (Exception suppressRedisException)
- {
- Log.Error("Could not append exception to redis service error logs", suppressRedisException);
- }
- }
- }
-
- var errorResponse = DtoUtils.CreateErrorResponse(request, ex, responseStatus);
-
- return errorResponse;
- }
-
- /// <summary>
- /// Override to provide additional/less context about the Service Exception.
- /// By default the request is serialized and appended to the ResponseStatus StackTrace.
- /// </summary>
- public virtual string GetRequestErrorBody(object request)
- {
- var requestString = "";
- try
- {
- requestString = TypeSerializer.SerializeToString(request);
- }
- catch /*(Exception ignoreSerializationException)*/
- {
- //Serializing request successfully is not critical and only provides added error info
- }
-
- return string.Format("[{0}: {1}]:\n[REQUEST: {2}]", GetType().Name, DateTime.UtcNow, requestString);
- }
- }
-}
View
5 src/ServiceStack.ServiceInterface/Providers/InMemoryRollingRequestLogger.cs
@@ -104,10 +104,7 @@ public static object ToSerializableErrorResponse(object response)
return errorResult.Response;
var ex = response as Exception;
- if (ex != null)
- ResponseStatusTranslator.Instance.Parse(ex);
-
- return null;
+ return ex != null ? ex.ToResponseStatus() : null;
}
}
}
View
6 src/ServiceStack.ServiceInterface/ServiceBase.cs
@@ -4,7 +4,9 @@
using ServiceStack.Common.Web;
using ServiceStack.Logging;
using ServiceStack.Messaging;
+using ServiceStack.ServiceClient.Web;
using ServiceStack.ServiceHost;
+using ServiceStack.ServiceInterface.Validation;
using ServiceStack.Text;
using ServiceStack.WebHost.Endpoints;
@@ -236,7 +238,7 @@ protected virtual object HandleException(TRequest request, Exception ex)
var errorResponse = useAppHost != null && useAppHost.ServiceExceptionHandler != null
? useAppHost.ServiceExceptionHandler(request, ex)
- : ErrorHandler.Instance.HandleException(useAppHost, request, ex);
+ : DtoUtils.HandleException(useAppHost, request, ex);
AfterEachRequest(request, errorResponse ?? ex);
@@ -293,7 +295,7 @@ public virtual object ExecuteAsync(TRequest request)
producer.Publish(request);
}
- return DtoUtils.CreateResponseDto(request);
+ return WebRequestUtils.GetErrorResponseDtoType(request).CreateInstance();
}
}
View
69 src/ServiceStack.ServiceInterface/ServiceModel/ResponseStatusTranslator.cs
@@ -1,69 +0,0 @@
-/*
-// $Id: ResponseStatusTranslator.cs 12245 2010-02-23 14:55:31Z Demis Bellot $
-//
-// Revision : $Revision: 12245 $
-// Modified Date : $LastChangedDate: 2010-02-23 14:55:31 +0000 (Tue, 23 Feb 2010) $
-// Modified By : $LastChangedBy: Demis Bellot $
-//
-// (c) Copyright 2012 ServiceStack
-*/
-
-using System;
-using ServiceStack.Common.Extensions;
-using ServiceStack.DesignPatterns.Translator;
-using ServiceStack.FluentValidation;
-using ServiceStack.ServiceHost;
-using ServiceStack.Validation;
-
-namespace ServiceStack.ServiceInterface.ServiceModel
-{
- /// <summary>
- /// Translates a ValidationResult into a ResponseStatus DTO fragment.
- /// </summary>
- public class ResponseStatusTranslator
- : ITranslator<ResponseStatus, ValidationErrorResult>
- {
- public static readonly ResponseStatusTranslator Instance
- = new ResponseStatusTranslator();
-
- public ResponseStatus Parse(Exception exception)
- {
- var validationError = exception as ValidationError;
- if (validationError != null)
- {
- return this.Parse(validationError);
- }
-
- var validationException = exception as ValidationException;
- if (validationException != null)
- {
- return this.Parse(validationException);
- }
-
- var httpError = exception as IHttpError;
- return httpError != null
- ? DtoUtils.CreateErrorResponse(httpError.ErrorCode, httpError.Message)
- : DtoUtils.CreateErrorResponse(exception.GetType().Name, exception.Message);
- }
-
- public ResponseStatus Parse(ValidationError validationException)
- {
- return DtoUtils.CreateErrorResponse(validationException.ErrorCode, validationException.Message, validationException.Violations);
- }
-
- public ResponseStatus Parse(ValidationException validationException)
- {
- var errors = validationException.Errors.ConvertAll(x =>
- new ValidationErrorField(x.ErrorCode, x.PropertyName, x.ErrorMessage));
-
- return DtoUtils.CreateErrorResponse(typeof(ValidationException).Name, validationException.Message, errors);
- }
-
- public ResponseStatus Parse(ValidationErrorResult validationResult)
- {
- return validationResult.IsValid
- ? DtoUtils.CreateSuccessResponse(validationResult.SuccessMessage)
- : DtoUtils.CreateErrorResponse(validationResult.ErrorCode, validationResult.ErrorMessage, validationResult.Errors);
- }
- }
-}
View
3 src/ServiceStack.ServiceInterface/ServiceStack.ServiceInterface.csproj
@@ -125,7 +125,6 @@
<Compile Include="Auth\UnAssignRolesService.cs" />
<Compile Include="Cors\CorsFeature.cs" />
<Compile Include="Cors\CorsSupportAttribute.cs" />
- <Compile Include="ErrorHandler.cs" />
<Compile Include="Providers\InMemoryRollingRequestLogger.cs" />
<Compile Include="Admin\RequestLogsService.cs" />
<Compile Include="RequiredRoleAttribute.cs" />
@@ -215,10 +214,8 @@
<Compile Include="RestServiceBase.cs" />
<Compile Include="ServiceBase.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="ServiceModel\ResponseStatusTranslator.cs" />
<Compile Include="ServiceResponseException.cs" />
<Compile Include="ServiceRoutesExtensions.cs" />
- <Compile Include="ServiceUtils.cs" />
<Compile Include="SessionFactory.cs" />
<Compile Include="SessionExtensions.cs" />
<Compile Include="SessionFeature.cs" />
View
21 src/ServiceStack.ServiceInterface/ServiceUtils.cs
@@ -1,21 +0,0 @@
-using ServiceStack.ServiceHost;
-using ServiceStack.ServiceInterface.ServiceModel;
-using ServiceStack.Validation;
-
-namespace ServiceStack.ServiceInterface
-{
- public static class ServiceUtils
- {
- public static object CreateErrorResponse<TRequest>(TRequest request, ValidationErrorResult validationError)
- {
- var responseStatus = ResponseStatusTranslator.Instance.Parse(validationError);
-
- var errorResponse = DtoUtils.CreateErrorResponse(
- request,
- new ValidationError(validationError),
- responseStatus);
-
- return errorResponse;
- }
- }
-}
View
47 src/ServiceStack.ServiceInterface/Validation/ValidationFeature.cs
@@ -1,10 +1,12 @@
using System;
-using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Funq;
+using ServiceStack.Common.Extensions;
using ServiceStack.FluentValidation;
+using ServiceStack.Logging;
using ServiceStack.ServiceHost;
+using ServiceStack.Validation;
using ServiceStack.WebHost.Endpoints;
using ServiceStack.Text;
@@ -12,7 +14,11 @@ namespace ServiceStack.ServiceInterface.Validation
{
public class ValidationFeature : IPlugin
{
+ private static readonly ILog Log = LogManager.GetLogger(typeof(ValidationFeature));
+
public static bool Enabled { private set; get; }
+ private IAppHost appHost;
+ private HandleServiceExceptionDelegate existingHandler;
/// <summary>
/// Activate the validation mechanism, so every request DTO with an existing validator
@@ -23,7 +29,46 @@ public void Register(IAppHost appHost)
{
Enabled = true;
var filter = new ValidationFilters();
+ this.appHost = appHost;
appHost.RequestFilters.Add(filter.RequestFilter);
+
+ existingHandler = appHost.ServiceExceptionHandler;
+ appHost.ServiceExceptionHandler = HandleException;
+ }
+
+ public object HandleException(object request, Exception ex)
+ {
+ var validationException = ex as ValidationException;
+ if (validationException != null)
+ {
+ var errors = validationException.Errors.ConvertAll(x =>
+ new ValidationErrorField(x.ErrorCode, x.PropertyName, x.ErrorMessage));
+
+ return DtoUtils.CreateErrorResponse(typeof(ValidationException).Name, validationException.Message, errors);
+ }
+
+ return existingHandler != null
+ ? existingHandler(request, ex)
+ : DtoUtils.HandleException(appHost, request, ex);
+ }
+
+ /// <summary>
+ /// Override to provide additional/less context about the Service Exception.
+ /// By default the request is serialized and appended to the ResponseStatus StackTrace.
+ /// </summary>
+ public virtual string GetRequestErrorBody(object request)
+ {
+ var requestString = "";
+ try
+ {
+ requestString = TypeSerializer.SerializeToString(request);
+ }
+ catch /*(Exception ignoreSerializationException)*/
+ {
+ //Serializing request successfully is not critical and only provides added error info
+ }
+
+ return string.Format("[{0}: {1}]:\n[REQUEST: {2}]", GetType().Name, DateTime.UtcNow, requestString);
}
}
View
27 src/ServiceStack.ServiceInterface/Validation/ValidationFilters.cs
@@ -9,23 +9,22 @@ public class ValidationFilters
public void RequestFilter(IHttpRequest req, IHttpResponse res, object requestDto)
{
var validator = ValidatorCache.GetValidator(req, requestDto.GetType());
- if (validator != null)
- {
- var validatorWithHttpRequest = validator as IRequiresHttpRequest;
- if (validatorWithHttpRequest != null)
- validatorWithHttpRequest.HttpRequest = req;
+ if (validator == null) return;
- string ruleSet = req.HttpMethod;
- var validationResult = validator.Validate(
- new ValidationContext(requestDto, null, new MultiRuleSetValidatorSelector(ruleSet)));
+ var validatorWithHttpRequest = validator as IRequiresHttpRequest;
+ if (validatorWithHttpRequest != null)
+ validatorWithHttpRequest.HttpRequest = req;
- if (validationResult.IsValid) return;
+ var ruleSet = req.HttpMethod;
+ var validationResult = validator.Validate(
+ new ValidationContext(requestDto, null, new MultiRuleSetValidatorSelector(ruleSet)));
- var errorResponse = ServiceUtils.CreateErrorResponse(
- requestDto, validationResult.ToErrorResult());
+ if (validationResult.IsValid) return;
- res.WriteToResponse(req, errorResponse);
- }
- }
+ var errorResponse = DtoUtils.CreateErrorResponse(
+ requestDto, validationResult.ToErrorResult());
+
+ res.WriteToResponse(req, errorResponse);
+ }
}
}
View
105 src/ServiceStack/ServiceHost/DtoUtils.cs
@@ -4,6 +4,8 @@
using ServiceStack.Common.Utils;
using ServiceStack.Common.Web;
using ServiceStack.Logging;
+using ServiceStack.Redis;
+using ServiceStack.ServiceClient.Web;
using ServiceStack.ServiceInterface.ServiceModel;
using ServiceStack.Text;
using ServiceStack.Validation;
@@ -20,11 +22,6 @@ public static class DtoUtils
/// </summary>
public const string ResponseStatusPropertyName = "ResponseStatus";
- /// <summary>
- /// Naming convention for the request's Response DTO
- /// </summary>
- public const string ResponseDtoSuffix = "Response";
-
public static ResponseStatus ToResponseStatus(this Exception exception)
{
var validationError = exception as ValidationError;
@@ -44,25 +41,11 @@ public static ResponseStatus ToResponseStatus(this ValidationError validationExc
return CreateErrorResponse(validationException.ErrorCode, validationException.Message, validationException.Violations);
}
- /// <summary>
- /// Create an instance of the response dto based on the requestDto type and default naming convention
- /// </summary>
- /// <param name="request"></param>
- /// <returns></returns>
- public static object CreateResponseDto<TRequest>(TRequest request)
+ public static ResponseStatus ToResponseStatus(this ValidationErrorResult validationResult)
{
- // Get the type
- var responseDtoType = AssemblyUtils.FindType(GetResponseDtoName(request));
-
- if (responseDtoType == null)
- {
- // We don't support creation of response messages without a predictable type name
- return null;
- }
-
- // Create an instance of the response message for this request
- var responseDto = ReflectionUtils.CreateInstance(responseDtoType);
- return responseDto;
+ return validationResult.IsValid
+ ? CreateSuccessResponse(validationResult.SuccessMessage)
+ : CreateErrorResponse(validationResult.ErrorCode, validationResult.ErrorMessage, validationResult.Errors);
}
public static ResponseStatus CreateSuccessResponse(string message)
@@ -81,6 +64,18 @@ public static ResponseStatus CreateErrorResponse(string errorCode, string errorM
return CreateErrorResponse(errorCode, errorMessage, null);
}
+ public static object CreateErrorResponse<TRequest>(TRequest request, ValidationErrorResult validationError)
+ {
+ var responseStatus = validationError.ToResponseStatus();
+
+ var errorResponse = CreateErrorResponse(
+ request,
+ new ValidationError(validationError),
+ responseStatus);
+
+ return errorResponse;
+ }
+
public static object CreateErrorResponse<TRequest>(TRequest request, Exception ex, ResponseStatus responseStatus)
{
var responseDto = CreateResponseDto(request, responseStatus);
@@ -114,10 +109,8 @@ public static object CreateErrorResponse<TRequest>(TRequest request, Exception e
public static object CreateResponseDto<TRequest>(TRequest request, ResponseStatus responseStatus)
{
// Predict the Response message type name
- // Get the type
- var responseDtoType = AssemblyUtils.FindType(GetResponseDtoName(request));
- var responseDto = CreateResponseDto(request);
-
+ var responseDtoType = WebRequestUtils.GetErrorResponseDtoType(request);
+ var responseDto = responseDtoType.CreateInstance();
if (responseDto == null)
return null;
@@ -129,9 +122,7 @@ public static object CreateResponseDto<TRequest>(TRequest request, ResponseStatu
}
else
{
- // Get the ResponseStatus property
var responseStatusProperty = responseDtoType.GetProperty(ResponseStatusPropertyName);
-
if (responseStatusProperty != null)
{
// Set the ResponseStatus
@@ -143,13 +134,6 @@ public static object CreateResponseDto<TRequest>(TRequest request, ResponseStatu
return responseDto;
}
- public static string GetResponseDtoName<TRequest>(TRequest request)
- {
- return typeof(TRequest) != typeof(object)
- ? typeof(TRequest).FullName + ResponseDtoSuffix
- : request.GetType().FullName + ResponseDtoSuffix;
- }
-
/// <summary>
/// Creates the error response from the values provided.
///
@@ -198,7 +182,7 @@ public static ResponseStatus CreateErrorResponse(string errorCode, string errorM
return to;
}
- public static object HandleException<TRequest>(IAppHost appHost, TRequest request, Exception ex)
+ public static object HandleException(IAppHost appHost, object request, Exception ex)
{
if (ex.InnerException != null && !(ex is IHttpError))
ex = ex.InnerException;
@@ -213,12 +197,59 @@ public static object HandleException<TRequest>(IAppHost appHost, TRequest reques
Log.Error("ServiceBase<TRequest>::Service Exception", ex);
+ if (appHost != null)
+ LogErrorInRedisIfExists(appHost.TryResolve<IRedisClientsManager>(), request.GetType().Name, responseStatus);
+
var errorResponse = CreateErrorResponse(request, ex, responseStatus);
return errorResponse;
}
/// <summary>
+ /// Service error logs are kept in 'urn:ServiceErrors:{ServiceName}'
+ /// </summary>
+ public const string UrnServiceErrorType = "ServiceErrors";
+
+ /// <summary>
+ /// Combined service error logs are maintained in 'urn:ServiceErrors:All'
+ /// </summary>
+ public const string CombinedServiceLogId = "All";
+
+ public static void LogErrorInRedisIfExists(
+ IRedisClientsManager redisManager, string operationName, ResponseStatus responseStatus)
+ {
+ //If Redis is configured, maintain rolling service error logs in Redis (an in-memory datastore)
+ if (redisManager == null) return;
+ try
+ {
+ //Get a thread-safe redis client from the client manager pool
+ using (var client = redisManager.GetClient())
+ {
+ //Get a client with a native interface for storing 'ResponseStatus' objects
+ var redis = client.GetTypedClient<ResponseStatus>();
+
+ //Store the errors in predictable Redis-named lists i.e.
+ //'urn:ServiceErrors:{ServiceName}' and 'urn:ServiceErrors:All'
+ var redisSeriviceErrorList = redis.Lists[UrnId.Create(UrnServiceErrorType, operationName)];
+ var redisCombinedErrorList = redis.Lists[UrnId.Create(UrnServiceErrorType, CombinedServiceLogId)];
+
+ //Append the error at the start of the service-specific and combined error logs.
+ redisSeriviceErrorList.Prepend(responseStatus);
+ redisCombinedErrorList.Prepend(responseStatus);
+
+ //Clip old error logs from the managed logs
+ const int rollingErrorCount = 1000;
+ redisSeriviceErrorList.Trim(0, rollingErrorCount);
+ redisCombinedErrorList.Trim(0, rollingErrorCount);
+ }
+ }
+ catch (Exception suppressRedisException)
+ {
+ Log.Error("Could not append exception to redis service error logs", suppressRedisException);
+ }
+ }
+
+ /// <summary>
/// Override to provide additional/less context about the Service Exception.
/// By default the request is serialized and appended to the ResponseStatus StackTrace.
/// </summary>
View
5 src/ServiceStack/ServiceHost/ServiceRunner.cs
@@ -4,6 +4,7 @@
using ServiceStack.Common.Web;
using ServiceStack.Logging;
using ServiceStack.Messaging;
+using ServiceStack.ServiceClient.Web;
using ServiceStack.Text;
using ServiceStack.WebHost.Endpoints;
@@ -142,7 +143,7 @@ public virtual object HandleException(IRequestContext requestContext, TRequest r
//TODO workout validation errors
var errorResponse = useAppHost != null && useAppHost.ServiceExceptionHandler != null
? useAppHost.ServiceExceptionHandler(request, ex)
- : DtoUtils.HandleException(GetAppHost(), request, ex);
+ : DtoUtils.HandleException(useAppHost, request, ex);
AfterEachRequest(requestContext, request, errorResponse ?? ex);
@@ -164,7 +165,7 @@ public object ExecuteAsync(IRequestContext requestContext, object instance, TReq
producer.Publish(request);
}
- return DtoUtils.CreateResponseDto(request);
+ return WebRequestUtils.GetErrorResponseDtoType(request).CreateInstance();
}
//signature matches ServiceExecFn
View
86 tests/RazorRockstars.Console.Files/ReqStarsService.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
@@ -10,6 +11,7 @@
using ServiceStack.ServiceClient.Web;
using ServiceStack.ServiceHost;
using ServiceStack.ServiceInterface;
+using ServiceStack.ServiceInterface.ServiceModel;
using ServiceStack.Text;
namespace RazorRockstars.Console.Files
@@ -34,6 +36,14 @@ public class SearchReqstars : IReturn<ReqstarsResponse>
public int? Age { get; set; }
}
+ public class ReqstarsResponse
+ {
+ public int Total { get; set; }
+ public int? Aged { get; set; }
+ public List<Reqstar> Results { get; set; }
+ public ResponseStatus ResponseStatus { get; set; }
+ }
+
[Route("/reqstars/reset")]
public class ResetReqstar : IReturnVoid { }
@@ -50,7 +60,7 @@ public class DeleteReqstar : IReturnVoid
}
[Route("/reqstars")]
- public class Reqstar : IReturn<ReqstarsResponse>
+ public class Reqstar : IReturn<List<Reqstar>>
{
public int Id { get; set; }
public string FirstName { get; set; }
@@ -67,14 +77,6 @@ public Reqstar(int id, string firstName, string lastName, int age)
}
}
- [Csv(CsvBehavior.FirstEnumerable)]
- public class ReqstarsResponse
- {
- public int Total { get; set; }
- public int? Aged { get; set; }
- public List<Reqstar> Results { get; set; }
- }
-
public class ReqstarsService : Service
{
@@ -92,6 +94,9 @@ public void Any(ResetReqstar request)
public object Get(SearchReqstars request)
{
+ if (request.Age.HasValue && request.Age <= 0)
+ throw new ArgumentException("Invalid Age");
+
return new ReqstarsResponse //matches ReqstarsResponse.cshtml razor view
{
Aged = request.Age,
@@ -110,8 +115,11 @@ public object Get(GetReqstar request)
public object Post(Reqstar request)
{
+ if (!request.Age.HasValue)
+ throw new ArgumentException("Age is required");
+
Db.Insert(request.TranslateTo<Reqstar>());
- return Get(new SearchReqstars());
+ return Db.Select<Reqstar>();
}
public void Any(DeleteReqstar request)
@@ -130,6 +138,8 @@ public class ReqStarsServiceTests
private const string BaseUri = Host + "/";
JsonServiceClient client;
+ //XmlServiceClient client;
+ //JsvServiceClient client;
private AppHost appHost;
@@ -205,6 +215,29 @@ public void Can_GET_SearchReqstars_PrettyRestApi()
Assert.That(response.Results.Count, Is.EqualTo(ReqstarsService.SeedData.Length));
}
+ [Test]
+ public void Invalid_GET_SearchReqstars_throws_typed_Response_PrettyRestApi()
+ {
+ try
+ {
+ var response = client.Get(new SearchReqstars { Age = -1 });
+ Assert.Fail("POST's to SearchReqstars should not be allowed");
+ }
+ catch (WebServiceException webEx)
+ {
+ Assert.That(webEx.StatusCode, Is.EqualTo(400));
+ Assert.That(webEx.StatusDescription, Is.EqualTo("ArgumentException"));
+
+ Assert.That(webEx.ResponseStatus, Is.Not.Null);
+ Assert.That(webEx.ResponseDto, Is.Not.Null);
+
+ var typedError = webEx.ResponseDto as ReqstarsResponse;
+ Assert.That(typedError, Is.Not.Null);
+ Assert.That(typedError.ResponseStatus.ErrorCode, Is.EqualTo("ArgumentException"));
+ Assert.That(typedError.ResponseStatus.Message, Is.EqualTo("Invalid Age"));
+ }
+ }
+
[Test]
public void Can_GET_SearchReqstars_aged_20()
@@ -278,10 +311,10 @@ public void Can_DELETE_Reqstar_PrettyRestApi()
[Test]
public void Can_CREATE_Reqstar()
{
- var response = client.Post<ReqstarsResponse>("/reqstars",
+ var response = client.Post<List<Reqstar>>("/reqstars",
new Reqstar(4, "Just", "Created", 25));
- Assert.That(response.Results.Count,
+ Assert.That(response.Count,
Is.EqualTo(ReqstarsService.SeedData.Length + 1));
}
@@ -290,7 +323,7 @@ public void Can_CREATE_Reqstar_PrettyTypedApi()
{
var response = client.Send(new Reqstar(4, "Just", "Created", 25));
- Assert.That(response.Results.Count,
+ Assert.That(response.Count,
Is.EqualTo(ReqstarsService.SeedData.Length + 1));
}
@@ -299,10 +332,31 @@ public void Can_CREATE_Reqstar_PrettyRestApi()
{
var response = client.Post(new Reqstar(4, "Just", "Created", 25));
- Assert.That(response.Results.Count,
+ Assert.That(response.Count,
Is.EqualTo(ReqstarsService.SeedData.Length + 1));
}
+ [Test]
+ public void Fails_to_CREATE_Empty_Reqstar_PrettyRestApi()
+ {
+ try
+ {
+ var response = client.Post(new Reqstar());
+ Assert.Fail("Should've thrown 400 Bad Request Error");
+ }
+ catch (WebServiceException webEx)
+ {
+ Assert.That(webEx.StatusCode, Is.EqualTo(400));
+ Assert.That(webEx.StatusDescription, Is.EqualTo("ArgumentException"));
+
+ Assert.That(webEx.ResponseStatus, Is.Not.Null);
+
+ var responseDto = webEx.ResponseDto as ErrorResponse;
+ Assert.That(responseDto, Is.Not.Null);
+ Assert.That(responseDto.ResponseStatus.ErrorCode, Is.EqualTo("ArgumentException"));
+ Assert.That(responseDto.ResponseStatus.Message, Is.EqualTo("Age is required"));
+ }
+ }
[Test]
public void Can_GET_ResetReqstars()
@@ -317,7 +371,7 @@ public void Can_GET_ResetReqstars()
}
[Test]
- public void Can_GET_ResetReqstars_PrettyTypedApi()
+ public void Can_SEND_ResetReqstars_PrettyTypedApi()
{
db.DeleteAll<Reqstar>();
View
3 tests/ServiceStack.WebHost.Endpoints.Tests/CustomerServiceValidationTests.cs
@@ -7,6 +7,7 @@
using System.Text.RegularExpressions;
using Funq;
using NUnit.Framework;
+using ServiceStack.Common.Web;
using ServiceStack.ServiceClient.Web;
using ServiceStack.FluentValidation;
using ServiceStack.Service;
@@ -153,7 +154,7 @@ private static List<ResponseError> GetValidationFieldErrors(string httpMethod, C
var validationResult = validator.Validate(
new ValidationContext(request, null, new MultiRuleSetValidatorSelector(httpMethod)));
- var responseStatus = ResponseStatusTranslator.Instance.Parse(validationResult.ToErrorResult());
+ var responseStatus = validationResult.ToErrorResult().ToResponseStatus();
var errorFields = responseStatus.Errors;
return errorFields ?? new List<ResponseError>();

0 comments on commit 1874900

Please sign in to comment.
Something went wrong with that request. Please try again.