Веб-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;
        }
Другие вопросы по тегам