asp.net Web API most efficient way to replace a re

2019-08-09 08:51发布

问题:

I am using as Web API what uses AuthorisationManager owin middleware to handle token based security.

My problem is that various errors within the response body have various different formats.

Within my api, I usually send errors back with the structure

 {"code": "error code", "message": "error message"}

However some of the errors coming from the security may use

 {"error": "error code", "error_description": "error message"}

or sometimes just

 {"error": "error mesage"}

I would like to unify these to all have the same structure I use elsewhere, ie the

{"code": "error code", "message": "error message"}

I have seen quite a few posts on replacing a response body.

I first tried this method, ie using the DelegatingHandler. This worked in most cases, but it did not catch my authorization failed error messages comding out of my OAuthAuthorizationServerProvider

I next tried using a middleware approach as shown here.

Here is my full interpretation..

    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());      
        }

So this basically seemed to work, and catch ALL responses, which is good.

However, what I was hoping to do, is, when there is no error, (or the error is already in the correct format), just pass the response on without doing anything with it.

I am mainly thinking of some of my GETs, where the data may be large, I was hoping to avoid having to do the extra copying back. In the above code, where I have marked *** COMMENT1 ***, I have an early return to try to avoid this, ie the line...

    // Error codes start at 400. If we have no errors, no more to d0.
    if (context.Response.StatusCode < 400)
        return;

The problem, is when I do this, I get no body at all returned, ie no data for all the GET calls, etc.

Is there a way to avoid this extra copying (ie at the line *** COMMENT2 ***) when we don't want to do any modifications?

Thanks in advance for any suggestions.

回答1:

Adding an answer since it has a code snippet but this is really just a comment.

Our services use the delegatingHandler approach you mentioned you tried first. Do you have try/catch around the call to base.SendAsync. In this snippet the requestState is just a wrapper around the incoming request with some timers, loggers, etc. In many cases we replace the response as you are trying. I stepped through the exception and used the VS debugger immediate window to modify the error response. It works for me.(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;
        }