改进API错误响应:使用Result模式提升可靠性和可扩展性

21,389次阅读
没有评论

共计 7508 个字符,预计需要花费 19 分钟才能阅读完成。

了解结果模式如何增强 API 错误处理。学习提高 API 可靠性、可扩展性等方面的实用技巧。

改进 API 错误响应

在不断扩大的 API 世界中,有意义的错误响应与结构良好的成功响应同样重要。在这篇文章中,我将介绍一些我在 Raygun 工作期间遇到的创建响应的不同选项。我们将讨论一些常见选项的优缺点,并以我认为是 API 设计中最佳选择之一的结果模式作为结尾。这种模式可以使 API 清晰地处理错误状态,并轻松实现一致的未来端点开发。在开发最近发布的 Raygun API 项目时,它对我特别有用,因为它简化了处理错误状态所需的代码,从而加快了端点开发速度。

什么是 ” 有用 ” 的错误响应?

有用的错误响应提供开发人员纠正错误状态所需的所有信息。这可以通过提供有帮助的错误消息和一致使用 HTTP 状态码来实现。

创建响应的几种选择?

在这里,我将为您介绍几种不同的创建和处理错误响应的选项,每种选项的优缺点,并向您展示结果模式如何为您提供质量和一致的响应。在这些示例中,我使用的是 C# 和.NET,但相同的思想也可以应用于其他语言和框架。

选项一:空值检查

我们的第一个选项使用空值来指示请求发生了问题。

在下面的示例中,您可以看到当 CreateUser 函数无法成功创建用户时,它将返回一个 null 值。然后,我们使用这个作为返回 400 Bad Request 响应的指示。

[HttpPost]
public ActionResult CreateUser([FromServices] IUserService service, [FromBody] CreateUserRequest request)
{var createdUser = service.CreateUser(request);

  if (createdUser is null)
  {return Problem(detail: "无法创建用户", statusCode: StatusCodes.Status400BadRequest);
  }

 return createdUser;
}

在这个示例中,我们看到了如何使用结果模式来改进 API 的错误响应。当调用 CreateUser 函数创建用户时,如果返回的 createdUser 为 null,说明创建用户失败。在这种情况下,我们使用 Problem 方法返回一个带有错误详细信息和 HTTP 状态码为 400 的响应。

在 UserService 中,我们可能会有以下代码:

public User? CreateUser(CreateUserRequest request)
{if (!request.EmailAddress.Contains('@'))
  {return null;}
  // 省略部分代码
}

现在,如果我们使用不包含 @符号的电子邮件地址调用该端点,我们将收到以下响应:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "Failed to create user",
  "traceId": "00-d82c53bff5687d0a789092202b84c8e2-75c3503892c8f4e6-00"
}

表面上看,这种方法似乎没有任何问题,因为我们能够告诉开发人员他们的请求失败了。然而,由于错误消息模糊不清,他们将不知道请求失败的原因,也无法纠正请求。

优点:

缺点:

  • 提供的错误消息不明确和模糊

  • 所有控制器动作都需要显式处理 null 值

  • 返回的错误代码没有灵活性

  • 每个控制器动作定义了响应代码类型,存在使用错误代码不一致的潜在问题

这种方法的主要问题是缺乏明确的错误信息和灵活的错误代码处理。开发人员无法准确了解错误原因,也无法根据错误信息采取适当的措施。此外,由于缺乏一致性,不同的控制器动作可能会返回不同的错误代码,增加了代码维护和调试的难度。

选项二:将异常用作流程控制

在这个选项中,我们将探讨使用异常来捕获更多信息,以提供给用户更详细的错误消息和响应代码。

首先,我们创建一个异常类来描述发生的错误:

public class BadRequestException : Exception
{public BadRequestException(string message)
    : base(message)
  {}}

然后,我们可以更新我们的控制器动作来捕获新的异常:

[HttpPost]
public ActionResult CreateUser([FromServices] IUserService service, [FromBody] CreatUserRequest request)
{
  try
  {var createdUser = service.CreateUser(request);
    return Ok(createdUser);
  }
  catch (BadRequestException exception)
  {return Problem(detail: exception.Message, statusCode: StatusCodes.Status400BadRequest);
  }
}

最后,我们更新 UserService,在适当的情况下抛出异常:

public User CreateUser(CreatUserRequest request)
{if (!request.EmailAddress.Contains('@'))
  {throw new BadRequestException("EmailAddress must contain an'@'");
  }
  // 省略部分代码
}

现在,使用和之前相同的请求调用端点会返回以下响应:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Bad Request",
  "status": 400,
  "detail": "EmailAddress must contain an'@'","traceId":"00-94b344c12a6deae65a40aed65af6f26d-d056ed66a00e792f-00"
}

现在,我们能够告诉 API 的用户请求失败的原因,并根据响应来纠正他们的请求。

从这里开始,我们可以通过添加一个中间件来进一步改进,该中间件将捕获异常并创建 ”Bad Request” 响应。

public class ErrorResponseHandlingMiddleware
{
  private readonly RequestDelegate _next;
  public ErrorResponseHandlingMiddleware(RequestDelegate next)
  {_next = next;}
  public async Task InvokeAsync(HttpContext context)
  {
    try
    {await _next(context);
    }
    catch (BadRequestException ex)
    {var factory = context.RequestServices.GetRequiredService();
      var problemDetails = factory.CreateProblemDetails(context, detail: ex.Message, statusCode: StatusCodes.Status400BadRequest);
      await Results
       .Problem(problemDetails)
       .ExecuteAsync(context);
    }
  }
}

将中间件添加到 program.cs 文件中:

app.UseMiddleware();

现在,在控制器动作中不再需要 try/catch 块。

[HttpPost]
public ActionResult CreateUser([FromServices] IUserService service, [FromBody] CreatUserRequest request)
{var createdUser = service.CreateUser(request);
  return Ok(createdUser);
}

通过使用自定义异常类型,这种方法可以进一步扩展,允许您返回不同的响应代码。然而,引入异常作为执行流程控制的机制会带来许多其他潜在问题。这些问题在许多其他文章中都有详细探讨,所以我在这里不会详述,只是简要总结几点:

  • 使用异常作为流程控制会使代码执行路径更难跟踪。

  • 引发异常会带来性能开销。

  • 很难区分真实异常和用于流程控制的异常。

优点:

  • 错误消息具有信息性,因为它们可以在异常消息中定义

  • 错误响应代码灵活,通过处理不同的异常类型

  • 控制器动作简单

  • 通过使用自定义异常类型,响应代码可以在使用上更一致

缺点:

  • 引入异常作为流程控制的形式

  • 可能会干扰异常日志记录工具(如 Raygun!)

  • 中间件的使用使得代码执行难以跟踪响应是如何创建的

选项三:Result 模式

什么是结果 Result 模式?

Result 模式是一种在 API 设计中广泛应用的模式,它提供了一种更灵活和可扩展的方式来处理错误状态。

在这种模式下,我们创建一个包含成功或失败状态的结果对象,并将所有相关信息存储在该对象中。结果对象可以包含详细的错误消息、HTTP 状态码和其他自定义数据。

示例:

public class Result
{public bool IsSuccess { get;}
  public T Value {get;}
  public string ErrorMessage {get;}

  private Result(bool isSuccess, T value, string errorMessage)
  {
    IsSuccess = isSuccess;
    Value = value;
    ErrorMessage = errorMessage;
  }

  public static Result Success(T value)
  {return new Result(true, value, null);
  }

  public static Result Failure(string errorMessage)
  {return new Result(false, default(T), errorMessage);
  }
}

简单来说,Result 模式是指一个操作返回一个包含操作结果和操作返回的任何数据的对象。实现一个原始的结果类型相当简单,但在这里我使用 FluentResults,因为它是一个功能齐全的库,可以满足我的需求,而不需要自己创建。

什么是 FluentResults?

正如 GitHub 页面所说:

FluentResults 是一个轻量级的.NET 库,旨在解决一个常见问题。它返回一个表示操作成功或失败的对象,而不是抛出 / 使用异常。

示例:

安装包:

Install-Package FluentResults

更新代码如下:

1. 创建一个从 UserService 返回的 Result 类型

using FluentResults;
namespace Example.Errors
{
  public class RequestValidationError : Error
  {public RequestValidationError(string message)
      : base(message)
    {}}
}

2. 更新 UserService 以返回新的 Result 类型

public Result CreateUser(CreatUserRequest request)
{if (!request.Email.Contains('@'))
  {return new RequestValidationError("Email must contain a'@'");
  }
  // 省略部分代码
}

3. 更新控制器动作以处理新的返回类型:

[HttpPost]
public ActionResult CreateUser([FromServices] IUserService service, [FromBody] CreatUserRequest request)
{var createdUser = service.CreateUser(request);
  if (createdUser.IsFailed)
  {return Problem(detail: createdUser.Errors.First().Message, statusCode: StatusCodes.Status400BadRequest);
  }
  return Ok(createdUser.Value);
}

这将产生与使用异常的示例相同的结果,但不会使用异常。但与异常示例类似,我们可以将此逻辑提取出控制器动作。

为了从控制器动作中提取逻辑,我们可以创建一个基础控制器类,由我们的控制器扩展。

public class ResultsControllerBase : ControllerBase
{protected ActionResult Ok(IResult result)
  {if (result.IsSuccess)
    {return base.Ok(result.Value);
    }
    var error = result.Errors.First();
    if (error is RequestValidationError)
    {return Problem(detail: error.Message, statusCode: StatusCodes.Status400BadRequest);
    }
    // Throw - because we've got an error we haven't accounted for
    throw new Exception(result.ToString());
  }
}

现在,在 UserController 中扩展这个新的基类并从动作方法中移除逻辑。

public class UserController : ResultsControllerBase
{[HttpPost]
  public ActionResult CreateUser([FromServices] IUserService service, [FromBody] CreatUserRequest request)
  {var createdUser = service.CreateUser(request);
    return Ok(createdUser);
  }
}

进一步扩展时,可以根据错误的类型来确定要返回的 HTTP 响应类型。例如,如果已经存在用户,则 UserService 可以返回 ResourceConflictError,以允许控制器基类在这种情况下返回 409 Conflict 响应。

优点:

  • 错误消息具有信息性,因为它们可以由 Result 类型定义

  • 通过在基类中处理不同的 Result 类型,错误响应代码是灵活的

  • 控制器动作具有最少或没有逻辑

  • 通过处理不同的 Result 类型,响应代码可以更一致

  • 使用基类仍然简单地遵循执行路径

  • 允许服务对返回值和可能的错误进行明确说明

  • 允许领域指定在超出仅限于 HTTP 响应的上下文中有意义的错误

缺点:

  • 需要确保结果模式仅在需要时使用,以防止它进入整个代码库,因为它可能会导致无需的函数膨胀

  • 它不受.NET 的本机支持,而依赖于第三方库进一步扩展结果模式的好处还包括:

  • 可以定义多个不同类型的错误,以更准确地描述失败情况。

  • 提供了一种清晰的方式将操作成功和失败的数据捆绑在一起,避免了返回值和异常的混合使用。

  • 便于单元测试,可以轻松验证操作的结果和返回的数据。

然而,结果模式也有一些限制和注意事项:

  • 需要在整个应用程序中一致地使用结果模式来获得最大的效益。如果只在部分代码中使用结果模式,可能会导致代码库中存在混合使用结果模式和其他方法(如异常处理)的情况,增加了代码的复杂性。

  • - 使用第三方库会引入外部依赖,需要考虑其稳定性、维护性和性能。

  • 对于简单的操作和错误处理,使用结果模式可能会显得过于繁琐。在这种情况下,使用异常处理可能更加简洁和直观。

总的来说,结果模式是一种替代异常处理的有效方式,特别适用于需要详细错误消息、灵活的错误响应代码和统一的结果处理的场景。通过对结果类型进行扩展和自定义,可以根据具体的应用需求来优化结果模式的实现。

结论

在 API 中实现有用的错误响应不仅仅涉及高效地处理错误,还包括为与 API 交互的用户创建更直观和用户友好的界面。通过使用本文讨论的结果模式,您可以提供清晰、可行的错误消息,使开发人员能够独立理解和纠正问题。这种方法不仅增强了用户体验,还鼓励更好的 API 设计实践。此外,通过避免常见的反模式并采用更结构化的错误处理方法,可以确保您的 API 具有鲁棒性、可维护性和可扩展性。

关键词:API 错误响应,Result 模式,可靠性,可扩展性 文章来源地址 https://www.toymoban.com/diary/system/622.html

到此这篇关于改进 API 错误响应:使用 Result 模式提升可靠性和可扩展性的文章就介绍到这了, 更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持 TOY 模板网!

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于1970-01-01发表,共计7508字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)