Après avoir examiné un article Gestion des exceptions dans l'API Web ASP.NET , je suis un peu confus quant au moment de lever une exception ou de renvoyer une réponse d'erreur. Je reste également à me demander s'il est possible de modifier la réponse lorsque votre méthode renvoie un modèle spécifique à un domaine au lieu de HttpResponseMessage
...
Donc, pour récapituler, voici mes questions suivies d'un code avec des # de cas:
HttpResponseMessage
au lieu d'un modèle de domaine concret, afin que le message puisse être personnalisé?HttpResponseException
et Request.CreateErrorResponse
? La sortie vers le client semble identique ...HttpError
pour "insérer" les messages de réponse dans des erreurs (que l'exception soit levée ou qu'une réponse d'erreur soit renvoyée)?// CASE #1
public Customer Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundResponse);
}
//var response = Request.CreateResponse(HttpStatusCode.OK, customer);
//response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return customer;
}
// CASE #2
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var notFoundResponse = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundResponse);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}
// CASE #3
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var message = String.Format("customer with id: {0} was not found", id);
var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
throw new HttpResponseException(errorResponse);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}
// CASE #4
public HttpResponseMessage Get(string id)
{
var customer = _customerService.GetById(id);
if (customer == null)
{
var message = String.Format("customer with id: {0} was not found", id);
var httpError = new HttpError(message);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}
var response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
return response;
}
Pour aider à mieux démontrer les cas 2, 3, 4, l'extrait de code suivant met en évidence plusieurs options "pouvant se produire" lorsqu'un client n'est pas trouvé ...
if (customer == null)
{
// which of these 4 options is the best strategy for Web API?
// option 1 (throw)
var notFoundMessage = new HttpResponseMessage(HttpStatusCode.NotFound);
throw new HttpResponseException(notFoundMessage);
// option 2 (throw w/ HttpError)
var message = String.Format("Customer with id: {0} was not found", id);
var httpError = new HttpError(message);
var errorResponse = Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
throw new HttpResponseException(errorResponse);
// option 3 (return)
var message = String.Format("Customer with id: {0} was not found", id);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, message);
// option 4 (return w/ HttpError)
var message = String.Format("Customer with id: {0} was not found", id);
var httpError = new HttpError(message);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, httpError);
}
L'approche que j'ai adoptée consiste à simplement lever les exceptions des actions du contrôleur api et à enregistrer un filtre d'exception qui traite l'exception et définit une réponse appropriée sur le contexte d'exécution de l'action.
Le filtre présente une interface fluide qui permet d’enregistrer les gestionnaires pour des types spécifiques d’exceptions avant d’enregistrer le filtre avec une configuration globale.
L'utilisation de ce filtre permet la gestion centralisée des exceptions au lieu de l'étendre aux actions du contrôleur. Il existe cependant des cas où je vais intercepter des exceptions dans l’action du contrôleur et renvoyer une réponse spécifique s’il n’est pas logique de centraliser le traitement de cette exception particulière.
Exemple d'enregistrement du filtre:
GlobalConfiguration.Configuration.Filters.Add(
new UnhandledExceptionFilterAttribute()
.Register<KeyNotFoundException>(HttpStatusCode.NotFound)
.Register<SecurityException>(HttpStatusCode.Forbidden)
.Register<SqlException>(
(exception, request) =>
{
var sqlException = exception as SqlException;
if (sqlException.Number > 50000)
{
var response = request.CreateResponse(HttpStatusCode.BadRequest);
response.ReasonPhrase = sqlException.Message.Replace(Environment.NewLine, String.Empty);
return response;
}
else
{
return request.CreateResponse(HttpStatusCode.InternalServerError);
}
}
)
);
Classe UnhandledExceptionFilterAttribute:
using System;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Filters;
namespace Sample
{
/// <summary>
/// Represents the an attribute that provides a filter for unhandled exceptions.
/// </summary>
public class UnhandledExceptionFilterAttribute : ExceptionFilterAttribute
{
#region UnhandledExceptionFilterAttribute()
/// <summary>
/// Initializes a new instance of the <see cref="UnhandledExceptionFilterAttribute"/> class.
/// </summary>
public UnhandledExceptionFilterAttribute() : base()
{
}
#endregion
#region DefaultHandler
/// <summary>
/// Gets a delegate method that returns an <see cref="HttpResponseMessage"/>
/// that describes the supplied exception.
/// </summary>
/// <value>
/// A <see cref="Func{Exception, HttpRequestMessage, HttpResponseMessage}"/> delegate method that returns
/// an <see cref="HttpResponseMessage"/> that describes the supplied exception.
/// </value>
private static Func<Exception, HttpRequestMessage, HttpResponseMessage> DefaultHandler = (exception, request) =>
{
if(exception == null)
{
return null;
}
var response = request.CreateResponse<string>(
HttpStatusCode.InternalServerError, GetContentOf(exception)
);
response.ReasonPhrase = exception.Message.Replace(Environment.NewLine, String.Empty);
return response;
};
#endregion
#region GetContentOf
/// <summary>
/// Gets a delegate method that extracts information from the specified exception.
/// </summary>
/// <value>
/// A <see cref="Func{Exception, String}"/> delegate method that extracts information
/// from the specified exception.
/// </value>
private static Func<Exception, string> GetContentOf = (exception) =>
{
if (exception == null)
{
return String.Empty;
}
var result = new StringBuilder();
result.AppendLine(exception.Message);
result.AppendLine();
Exception innerException = exception.InnerException;
while (innerException != null)
{
result.AppendLine(innerException.Message);
result.AppendLine();
innerException = innerException.InnerException;
}
#if DEBUG
result.AppendLine(exception.StackTrace);
#endif
return result.ToString();
};
#endregion
#region Handlers
/// <summary>
/// Gets the exception handlers registered with this filter.
/// </summary>
/// <value>
/// A <see cref="ConcurrentDictionary{Type, Tuple}"/> collection that contains
/// the exception handlers registered with this filter.
/// </value>
protected ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> Handlers
{
get
{
return _filterHandlers;
}
}
private readonly ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>> _filterHandlers = new ConcurrentDictionary<Type, Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>>();
#endregion
#region OnException(HttpActionExecutedContext actionExecutedContext)
/// <summary>
/// Raises the exception event.
/// </summary>
/// <param name="actionExecutedContext">The context for the action.</param>
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
if(actionExecutedContext == null || actionExecutedContext.Exception == null)
{
return;
}
var type = actionExecutedContext.Exception.GetType();
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;
if (this.Handlers.TryGetValue(type, out registration))
{
var statusCode = registration.Item1;
var handler = registration.Item2;
var response = handler(
actionExecutedContext.Exception.GetBaseException(),
actionExecutedContext.Request
);
// Use registered status code if available
if (statusCode.HasValue)
{
response.StatusCode = statusCode.Value;
}
actionExecutedContext.Response = response;
}
else
{
// If no exception handler registered for the exception type, fallback to default handler
actionExecutedContext.Response = DefaultHandler(
actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
);
}
}
#endregion
#region Register<TException>(HttpStatusCode statusCode)
/// <summary>
/// Registers an exception handler that returns the specified status code for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to register a handler for.</typeparam>
/// <param name="statusCode">The HTTP status code to return for exceptions of type <typeparamref name="TException"/>.</param>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler has been added.
/// </returns>
public UnhandledExceptionFilterAttribute Register<TException>(HttpStatusCode statusCode)
where TException : Exception
{
var type = typeof(TException);
var item = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
statusCode, DefaultHandler
);
if (!this.Handlers.TryAdd(type, item))
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;
if (this.Handlers.TryRemove(type, out oldItem))
{
this.Handlers.TryAdd(type, item);
}
}
return this;
}
#endregion
#region Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
/// <summary>
/// Registers the specified exception <paramref name="handler"/> for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to register the <paramref name="handler"/> for.</typeparam>
/// <param name="handler">The exception handler responsible for exceptions of type <typeparamref name="TException"/>.</param>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception <paramref name="handler"/>
/// has been added.
/// </returns>
/// <exception cref="ArgumentNullException">The <paramref name="handler"/> is <see langword="null"/>.</exception>
public UnhandledExceptionFilterAttribute Register<TException>(Func<Exception, HttpRequestMessage, HttpResponseMessage> handler)
where TException : Exception
{
if(handler == null)
{
throw new ArgumentNullException("handler");
}
var type = typeof(TException);
var item = new Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>>(
null, handler
);
if (!this.Handlers.TryAdd(type, item))
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> oldItem = null;
if (this.Handlers.TryRemove(type, out oldItem))
{
this.Handlers.TryAdd(type, item);
}
}
return this;
}
#endregion
#region Unregister<TException>()
/// <summary>
/// Unregisters the exception handler for exceptions of type <typeparamref name="TException"/>.
/// </summary>
/// <typeparam name="TException">The type of exception to unregister handlers for.</typeparam>
/// <returns>
/// This <see cref="UnhandledExceptionFilterAttribute"/> after the exception handler
/// for exceptions of type <typeparamref name="TException"/> has been removed.
/// </returns>
public UnhandledExceptionFilterAttribute Unregister<TException>()
where TException : Exception
{
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> item = null;
this.Handlers.TryRemove(typeof(TException), out item);
return this;
}
#endregion
}
}
Le code source peut également être trouvé ici .
Si vous ne renvoyez pas HttpResponseMessage et que vous renvoyez plutôt les classes d'entité/modèle directement, une approche que j'ai trouvée utile consiste à ajouter la fonction utilitaire suivante à mon contrôleur
private void ThrowResponseException(HttpStatusCode statusCode, string message)
{
var errorResponse = Request.CreateErrorResponse(statusCode, message);
throw new HttpResponseException(errorResponse);
}
et appelez-le simplement avec le code d'état et le message appropriés
Cas 1
Cas n ° 2-4
Ceux-ci devraient être équivalents; HttpResponseException encapsule un HttpResponseMessage, qui est ce qui est renvoyé en tant que réponse HTTP.
par exemple, le cas n ° 2 pourrait être réécrit comme suit:
public HttpResponseMessage Get(string id)
{
HttpResponseMessage response;
var customer = _customerService.GetById(id);
if (customer == null)
{
response = new HttpResponseMessage(HttpStatusCode.NotFound);
}
else
{
response = Request.CreateResponse(HttpStatusCode.OK, customer);
response.Content.Headers.Expires = new DateTimeOffset(DateTime.Now.AddSeconds(300));
}
return response;
}
... mais si la logique de votre contrôleur est plus compliquée, le lancement d'une exception peut simplifier le flux de code.
HttpError vous donne un format cohérent pour le corps de la réponse et peut être sérialisé en JSON/XML/etc, mais ce n'est pas obligatoire. Par exemple, vous pouvez ne pas vouloir inclure un corps d'entité dans la réponse, ou un autre format.
Ne lève pas une exception HttpResponseException ou ne renvoie pas un HttpResponesMessage pour les erreurs - sauf si l'intention est de mettre fin à la demande avec ce résultat exact .
Les exceptions HttpResponseException ne sont pas traitées comme les autres exceptions . Ils sont non pris en compte par les filtres d'exception . Ils sont non pris en compte par le gestionnaire d'exceptions . Ils constituent un moyen sournois de glisser un HttpResponseMessage tout en terminant le flux d'exécution du code actuel.
évitez à l'aide du type HttpResponseException!
HttpResponseMessage ne sont pas des exceptions. Ils ne terminent pas le flux d'exécution du code actuel. Ils peuvent et non être filtrés comme des exceptions. Ils peuvent ne pas être consignés en tant qu'exceptions. Ils représentent un résultat valide - même une réponse 500 est "une réponse valide sans exception"!
Rendre la vie plus simple:
En cas d’exception/d’erreur, lancez une exception .NET normale ou un type d’exception d’application personnalisée ( non dérivé de HttpResponseException) avec l’erreur/réponse http souhaitée. 'propriétés telles qu'un code d'état - selon la gestion des exceptions normales .
Utilisez Filtres d'exception/Gestionnaires d'exception/Enregistreurs d'exceptions pour faire quelque chose de approprié avec ces cas exceptionnels: modification/ajout de codes d'état? ajouter des identifiants de suivi? inclure des traces de pile? bûche?
En évitant HttpResponseException , le traitement des "cas exceptionnels" est uniformisé et peut être traité dans le cadre du pipeline exposé! Par exemple, on peut transformer un 'NotFound' en 404 et un 'ArgumentException' en 400 et un 'NullReference' en 500 facilement et uniformément avec des exceptions au niveau de l'application, tout en permettant à l'extensibilité de fournir des "bases" telles que la journalisation des erreurs.
Un autre cas d'utilisation de HttpResponseException
au lieu de Response.CreateResponse(HttpStatusCode.NotFound)
, ou d'un autre code d'état d'erreur, est si vous avez des transactions en filtres d'action et que vous souhaitez que les transactions soient annulées lors du renvoi d'une réponse d'erreur au client. .
L'utilisation de Response.CreateResponse
n'annulera pas la transaction, contrairement à une exception.
Je tiens à souligner que, selon mon expérience, si une exception HttpResponseException est renvoyée au lieu de renvoyer un HttpResponseMessage dans une méthode webapi 2, si un appel est effectué immédiatement vers IIS Express, il expire ou renvoie 200 mais avec une erreur HTML dans la réponse. Le moyen le plus simple de tester cela consiste à appeler $ .ajax à une méthode qui lève une exception HttpResponseException. Dans errorCallBack, dans ajax, appelez immédiatement une autre méthode ou même une simple page http. Vous remarquerez que l'appel immédiat échouera. Si vous ajoutez un point d'arrêt ou une settimeout () dans l'erreur, rappelez pour retarder le deuxième appel d'une ou deux secondes, ce qui donnera au serveur le temps de le récupérer. Cela n'a pas d'importance, mais son effet est presque similaire à celui de l'exception HttpResponseException. Le thread d'écoute côté serveur se ferme puis redémarre, ce qui entraîne une fraction de seconde sans aucune acceptation de connexion par le serveur.
Mise à jour: La cause principale de l'expiration du délai de connexion Ajax dans le wierd est si un appel ajax est effectué assez rapidement, la même connexion TCP est utilisée. l'appel ajax du navigateur, mais avec cet appel, MS renvoyait une erreur d'objet introuvable, car dans Startup.Auth.vb, app.UserCookieAuthentication était activée; il tentait donc de renvoyer la réponse en interceptant et d'ajouter une redirection, mais en erreur avec Object not Instance Cette erreur était html, mais a été ajoutée à la réponse après le fait, donc si l'appel ajax a été effectué assez rapidement et si la même connexion TCP utilisée a été renvoyée au navigateur, puis ajoutée au début de la prochain appel. Pour quelque raison que ce soit Chrome, le violoniste a joué le mélange entre json et htm mais firefox a retourné la véritable erreur. Tellement bizarre mais le renifleur de paquets ou firefox était le seul moyen de suivre celle-ci.
De plus, si vous utilisez l’aide de l’API Web pour générer une aide automatique et que vous renvoyez HttpResponseMessage, vous devez ajouter un
[System.Web.Http.Description.ResponseType(typeof(CustomReturnedType))]
attribut à la méthode pour que l’aide génère correctement. ensuite
return Request.CreateResponse<CustomReturnedType>(objCustomeReturnedType)
ou sur erreur
return Request.CreateErrorResponse( System.Net.HttpStatusCode.InternalServerError, new Exception("An Error Ocurred"));
J'espère que cela aidera quelqu'un d'autre qui risque d'avoir un délai d'attente aléatoire ou un serveur indisponible immédiatement après le lancement d'une exception HttpResponseException.
De plus, renvoyer une exception HttpResponseException présente l’avantage supplémentaire de ne pas interrompre Visual Studio lorsqu’une exception non gérée est utile lorsque l’erreur renvoyée est le message AuthToken qui doit être actualisé dans une application à une seule page.
Mise à jour: je retire ma déclaration à propos de IIS expiration du délai expresse, il s’est avéré qu’il s’agissait d’une erreur de rappel ajax côté client, car Ajax 1.8 renvoyait $ .ajax () et renvoyait $ .ajax. () .then () retourne tous les deux une promesse mais pas la même promesse chaînée then () renvoie une nouvelle promesse qui a rendu l’ordre d’exécution erroné. Ainsi, lorsque la promesse then () a été complétée, le script a expiré. Bizarre, mais pas un IIS exprime un problème entre le clavier et le fauteuil.
J'aime réponse opposée
Quoi qu'il en soit, j'avais besoin d'un moyen d'attraper l'Exception héritée et cette solution ne répond pas à tous mes besoins.
Alors j'ai fini par changer la façon dont il gère OnException et c'est ma version
public override void OnException(HttpActionExecutedContext actionExecutedContext) {
if (actionExecutedContext == null || actionExecutedContext.Exception == null) {
return;
}
var type = actionExecutedContext.Exception.GetType();
Tuple<HttpStatusCode?, Func<Exception, HttpRequestMessage, HttpResponseMessage>> registration = null;
if (!this.Handlers.TryGetValue(type, out registration)) {
//tento di vedere se ho registrato qualche eccezione che eredita dal tipo di eccezione sollevata (in ordine di registrazione)
foreach (var item in this.Handlers.Keys) {
if (type.IsSubclassOf(item)) {
registration = this.Handlers[item];
break;
}
}
}
//se ho trovato un tipo compatibile, uso la sua gestione
if (registration != null) {
var statusCode = registration.Item1;
var handler = registration.Item2;
var response = handler(
actionExecutedContext.Exception.GetBaseException(),
actionExecutedContext.Request
);
// Use registered status code if available
if (statusCode.HasValue) {
response.StatusCode = statusCode.Value;
}
actionExecutedContext.Response = response;
}
else {
// If no exception handler registered for the exception type, fallback to default handler
actionExecutedContext.Response = DefaultHandler(actionExecutedContext.Exception.GetBaseException(), actionExecutedContext.Request
);
}
}
Le coeur est cette boucle où je vérifie si le type d'exception est une sous-classe d'un type enregistré.
foreach (var item in this.Handlers.Keys) {
if (type.IsSubclassOf(item)) {
registration = this.Handlers[item];
break;
}
}
mes2cents
En cas d'erreur, je voulais renvoyer une classe de détails d'erreur spécifique, dans le format demandé par le client au lieu de l'objet happy path.
Je souhaite que mes méthodes de contrôleur renvoient l’objet Happy Path spécifique au domaine et lèvent une exception sinon.
Le problème que j'ai eu était que les constructeurs HttpResponseException n'autorisent pas les objets de domaine.
C’est ce que j’ai finalement trouvé
public ProviderCollection GetProviders(string providerName)
{
try
{
return _providerPresenter.GetProviders(providerName);
}
catch (BadInputValidationException badInputValidationException)
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest,
badInputValidationException.Result));
}
}
Result
est une classe qui contient les détails de l'erreur, tandis que ProviderCollection
est le résultat de mon chemin heureux.
Autant que je sache, que vous leviez une exception ou que vous retourniez Request.CreateErrorResponse, le résultat est identique. Si vous regardez le code source de System.Web.Http.dll, vous verrez autant. Jetez un coup d’œil à ce résumé général et à une solution très similaire à celle que j’ai proposée: Web Api, HttpError et comportement des exceptions