Веб-API asp.net - наиболее эффективный способ заменить тело ответа на запрос, чтобы иметь согласованную структуру ошибок
Я использую в качестве веб-API, что использует AuthorisationManager
Промежуточное ПО для управления безопасностью на основе токенов.
Моя проблема в том, что различные ошибки в теле ответа имеют разные форматы.
В моем API я обычно отправляю ошибки обратно со структурой
{"code": "error code", "message": "error message"}
Однако некоторые ошибки, поступающие из системы безопасности, могут использовать
{"error": "error code", "error_description": "error message"}
или иногда просто
{"error": "error mesage"}
Я хотел бы объединить их, чтобы все имели ту же структуру, которую я использую в другом месте, то есть
{"code": "error code", "message": "error message"}
Я видел довольно много сообщений о замене тела ответа.
Я впервые попробовал этот метод, то есть используя DelegatingHandler
, Это работало в большинстве случаев, но это не ловило мои сообщения об ошибках авторизации, выходящие из моего OAuthAuthorizationServerProvider
Затем я попытался использовать подход промежуточного программного обеспечения, как показано здесь.
Вот моя полная интерпретация..
public override async Task Invoke(IOwinContext context)
{
try
{
// hold a reference to what will be the outbound/processed response stream object
var stream = context.Response.Body;
// create a stream that will be sent to the response stream before processing
using (var buffer = new MemoryStream())
{
// set the response stream to the buffer to hold the unaltered response
context.Response.Body = buffer;
// allow other middleware to respond
await this.Next.Invoke(context);
// Error codes start at 400. If we have no errors, no more to d0.
if (context.Response.StatusCode < 400) // <---- *** COMMENT1 ***
return;
// we have the unaltered response, go to start
buffer.Seek(0, SeekOrigin.Begin);
// read the stream
var reader = new StreamReader(buffer);
string responseBody = reader.ReadToEnd();
// If no response body, nothing to do
if (string.IsNullOrEmpty(responseBody))
return;
// If we have the correct error fields names, no more to do
JObject responseBodyJson = JObject.Parse(responseBody);
if (responseBodyJson.ContainsKey("code") && responseBodyJson.ContainsKey("message"))
return;
// Now we will look for the known error formats that we want to replace...
byte[] byteArray = null;
// The first one from the security module, errors come back as {error, error_description}.
// The contents are what we set (so are correct), we just want the fields names to be the standard {code, message}
var securityErrorDescription = responseBodyJson.GetValue("error_description");
var securityErrorCode = responseBodyJson.GetValue("error");
if (securityErrorDescription != null && securityErrorCode != null)
byteArray = CreateErrorObject(securityErrorCode.ToString(), securityErrorDescription.ToString());
// The next horrible format, is when a refresh token is just sends back an object with 'error'.
var refreshTokenError = responseBodyJson.GetValue("error");
if (refreshTokenError != null)
{
// We will give this our own error code
var error = m_resourceProvider.GetRefreshTokenAuthorisationError(refreshTokenError.ToString());
byteArray = CreateErrorObject(error.Item2, error.Item3);
}
else
{
byteArray = Encoding.ASCII.GetBytes(responseBody);
}
// Now replace the response (body) with our now contents
// <---- *** COMMENT2 ***
context.Response.ContentType = "application / json";
context.Response.ContentLength = byteArray.Length;
buffer.SetLength(0);
buffer.Write(byteArray, 0, byteArray.Length);
buffer.Seek(0, SeekOrigin.Begin);
buffer.CopyTo(stream);
}
}
catch (Exception ex)
{
m_logger.WriteError($"ResponseFormattingMiddleware {ex}");
context.Response.StatusCode = 500;
throw;
}
}
private byte[] CreateErrorObject(string code, string message)
{
JObject newMessage = new JObject();
newMessage["code"] = code;
newMessage["message"] = message;
return Encoding.ASCII.GetBytes(newMessage.ToString());
}
Так что это в основном, похоже, работает, и ловит ВСЕ ответы, что хорошо.
Однако то, что я надеялся сделать, - это когда ошибка отсутствует (или ошибка уже в правильном формате), просто передайте ответ, ничего не делая с ним.
Я в основном думаю о некоторых из моих GET, где данные могут быть большими, я надеялся избежать дополнительного копирования. В приведенном выше коде, где я отметил *** COMMENT1 ***
У меня есть досрочное возвращение, чтобы попытаться избежать этого, то есть линии...
// Error codes start at 400. If we have no errors, no more to d0.
if (context.Response.StatusCode < 400)
return;
Проблема в том, что когда я делаю это, я не получаю никакого тела, то есть никаких данных для всех вызовов GET и т. Д.
Есть ли способ, чтобы избежать этого дополнительного копирования (т.е. в строке *** COMMENT2 ***
) когда мы не хотим делать какие-либо модификации?
Спасибо заранее за любые предложения.
1 ответ
Добавление ответа, поскольку у него есть фрагмент кода, но на самом деле это просто комментарий.
Наши сервисы используют подход делегирования Хэндлера, который вы упомянули, что вы пробовали в первую очередь. Есть ли у вас попытаться / поймать вокруг звонка base.SendAsync
, В этом фрагменте requestState
это просто оболочка для входящего запроса с некоторыми таймерами, регистраторами и т. д. Во многих случаях мы заменяем ответ, когда вы пытаетесь. Я прошел через исключение и использовал немедленное окно VS отладчика, чтобы изменить ответ об ошибке. Меня устраивает.(TM)
try
{
return base
.SendAsync(request, cancellationToken)
.ContinueWith(
(task, requestState) => ((InstrumentedRequest)requestState).End(task),
instrumentedRequest,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default)
.Unwrap();
}
catch (Exception ex)
{
instrumentedRequest.PrematureFault(ex);
throw;
}