RESTAPI:当资源存在但已过期时,正确的响应格式和状态代码是什么?

RESTAPI: What is a correct response format and status code when a resource exists however is expired?

假设我们有资源Employee。 员工看起来像那样(从实体和数据库的角度来看):

{
    id: 123,
    name: "John Doe",
    // more props here
    workPermitExpireDateUTC: "1999-06-30 00:00:00"
}

当我们查询所有员工时,我们希望显示附加标志 isExpired 即时计算:

GET: /api/employees:

// HttpStatuCode: 200
[
    {
        id: 123,
        name: "John Doe",
        // more props here
        workPermitExpireDateUTC: "1999-06-30 00:00:00",
        workPermitExpired: true // calculated on a fly, based on system clock
    },
    {
        id: 456,
        name: "George Smith",
        // more props here
        workPermitExpireDateUTC: "2025-06-30 00:00:00",
        workPermitExpired: false // calculated on a fly, based on system clock
    }
]

当我查询单个未到期员工时

GET /api/employess/456 我希望得到 200 Response 员工数据 - 这很明显。

然而,如果我们考虑过期员工,我不想发送所有数据,因为客户端不需要知道它。所以我的想法是 return 像这样:

GET: /api/employees/123:

// HttpStatuCode: 200
{
    id: 123,
    workPermitExpired: true,
    errorMessage: "Employee work permit expired"
}

但感觉不对,因为 200 响应不应根据资源 ID 具有不同的结构。

如果url GET: /api/employees/123/non-expired和return 200如果没有过期,如果410过期呢?

从技术上讲,过期员工与员工的资源类型不同,例如

class ExpiredEmployee extends Employee

所以 200 是合适的,因为您不会返回 Employee。您返回的是 ExpiredEmployee.

如果客户正在寻找员工,大概他们不关心过期的员工,因为他们在技术上不是员工?所以主要的 url 将是:

GET /api/employess/456 对于未到期的员工

您可以根据业务线来设计 URL。为什么会有人想要找到过期的员工?他们需要未过期吗?如果他们过期了,为什么他们仍然被认为是雇员?如果客户的主要业务是与员工合作,那么为离职员工提供一个单独的资源端点似乎是合适的,因为单独的业务流程可能会为他们工作。

如果客户不希望员工过期,则返回的对象可能会使他们感到困惑。也许一个单独的业务端点适合离职员工:

GET /api/employess/expired/456 过期员工

大概是在寻找什么 456 已经知道它们已经过期并想更改它们的状态?

What is a correct response format and status code when a resource exists however is expired?

在 REST 中需要理解的一件重要事情是,我们的 API 是一个 facade,故意设计为使我们的域看起来像网络,以便所有通用网络工具“正常工作”。

换一种说法:HTTP 是 transporting documents over a network 域中的应用程序。当我们构建 REST API 时,我们正在做的是调整我们的域协议以适应与客户端交换文档的语言。

状态代码和 headers 属于 transport-documents-over-a-network 域。

GET: /api/employees/123

对此的正确响应在很大程度上取决于我们是否拥有该资源的当前表示(这是运输文件域的一个关注点)。 我们的域中那个表示的语义真的没有进入它。

如果我们有资源的当前表示,那么这是 很好

200 OK
Content-Type: application/json

{
    "id": 123,
    "workPermitExpired": true,
    "errorMessage": "Employee work permit expired"
}

当我们无法满足提供当前资源的请求时,我们通常会使用状态代码向通用组件指示响应 body 描述错误情况,而不是表示资源。

404 Not Found
Content-Type: application/json

{ "error": "Who is 123?" }

it doesn't feel right, since 200 response should not have different structure depending on resource id.

没错。这通常转化为更仔细地定义您的架构的方式。如果您希望在表示中打开和关闭某些字段,那么您的模式定义需要将这些字段描述为可选。例如,我们可能会描述一个“有效许可”模式和一个“过期许可”模式(在不同的密钥下),其中的约束条件是客户端应该期望其中之一,但不能同时期望两者。

另一种可能性是对不同的表示使用不同的内容类型。 RFC 6838 是你寻找细节的地方,但粗略的轮廓是我们将定义两个不同的模式,然后为每个模式分配一个不同的内容类型

  • application/prs.maciej-active-permit+json
  • application/prs.maciej-expired-permit+json

application/+json 告诉一般用途的消费者表示“只是”json 文档,但熟悉您的特定媒体类型的消费者将有更具体的理解。

RFC 7807 是这种方法的标准化演示,其中字段名称的特定语义被编码到 json 文档中,媒体类型 application/problem+json 告诉 general-purpose组件到底发生了什么。

您在 application/merge-patch+json and application/json-patch+json 中看到了类似的想法。