JSON-RPC v2 API。HTTP 层。API 响应的适当 HTTP 层状态代码是什么?

JSON-RPC v2 API. HTTP Layer. What are the appropriate HTTP Layer status codes for API responses?

提问人:suchislife 提问时间:8/8/2023 更新时间:9/14/2023 访问量:232

问:

我一直在开发一个 Deno 脚本,该脚本使用 Deno.serve() 实现 JSON-RPC v2 API,该 API 最近已稳定。虽然我已经设置了基本的 CRUD 操作和错误处理,但我不确定我应该在不同场景中使用的 HTTP 状态代码。

我上网发现,在 JSON-RPC 2.0 中,协议本身并没有严格定义特定 HTTP 状态码的使用。相反,它侧重于 JSON 有效负载,以传达成功和错误状态。但是,当 JSON-RPC 通过 HTTP 传输时,有一些常见的约定:

  • 200 OK:这是 JSON-RPC 响应最常用的状态代码,既适用于成功调用,也适用于导致 JSON-RPC 错误的调用。成功调用和错误之间的区别是在 JSON 有效负载本身中使用 and 字段进行的。resulterror
  • 500 内部服务器错误:当出现阻止处理 JSON-RPC 请求的服务器错误时,可以使用此功能。这是服务器端问题的一般指示。
  • 404 Not Found:虽然方法未找到错误(通常与有效负载中的错误通信)不常见,但如果找不到 JSON-RPC 端点本身,则可以使用此状态。200 OK-32601 Method not found
  • 400 错误请求:这可用于格式错误的请求,其中 JSON 无效或请求不符合 JSON-RPC 格式。

我使用了以下状态代码:

  • 成功操作 200
  • 400 表示客户端错误(如无效参数)
  • 404 表示未找到的路线
  • 501 表示未实现的方法

有人可以澄清或提供哪些状态代码适用于不同的 JSON-RPC v2 方案的参考吗?真的只有这4个吗?

201、204、401、403、405、415、503呢?这是我对HTTP被用作传输层以及我过度思考语义感到困惑的部分......

代码如下:

// Configuration (read-only!)
const config = {
  rpc: {
    service: {
      hostname: "0.0.0.0",
      port: 3000,
      routes: {
        root: "/service",
        service: "/service/v2",
        status: "/service/v2/status",
        docs: "/service/v2/docs",
      },
    },
  },
} as const;

// Config Type definition
type Config = typeof config;

// DataStore Type definition
type DataStore = {
  [key: string]: any;
};

// Params Type definitions
type CreateParams = {
  id: string;
  name?: string;
  age?: number;
};

type ReadParams = {
  id: string;
  name?: string;
  age?: number;
};

type ListParams = {
  startIndex: number;
  endIndex: number;
};

type UpdateParams = {
  id: string;
  name?: string;
  age?: number;
};

type DeleteParams = {
  id: string;
  name?: string;
  age?: number;
};

// Data store for CRUD operations
const DATA_STORE: DataStore = {};

// CRUD methods
const methods = {
  // METHOD -> CREATE
  create: (params: CreateParams) => {
    const { id } = params;
    if (DATA_STORE[id]) {
      throw new Error("ID already exists");
    }
    DATA_STORE[id] = params;
    return [{ success: true }];
  },
  // METHOD -> READ
  read: (params: ReadParams) => {
    const { id } = params;
    if (!DATA_STORE[id]) {
      throw new Error("ID not found");
    }
    return [DATA_STORE[id]];
  },
  // METHOD -> LIST
  list: (params: ListParams) => {
    const { startIndex, endIndex } = params;
    if (startIndex === undefined || endIndex === undefined) {
      throw new Error(
        "Both startIndex and endIndex are required for the list method",
      );
    }
    return Object.entries(DATA_STORE)
      .slice(startIndex, endIndex + 1)
      .map(([key, value]) => ({ [key]: value }));
  },
  // METHOD -> UPDATE
  update: (params: UpdateParams) => {
    const { id } = params;
    if (!DATA_STORE[id]) {
      throw new Error("ID not found");
    }
    DATA_STORE[id] = params;
    return [{ success: true }];
  },
  // METHOD -> DELETE
  delete: (params: DeleteParams) => {
    const { id } = params;
    if (!DATA_STORE[id]) {
      throw new Error("ID not found");
    }
    delete DATA_STORE[id];
    return [{ success: true }];
  },
};

// REQUEST -> HANDLER
async function handler(request: Request): Promise<Response> {
  const reqUrl = request.url as string;
  const { pathname } = new URL(reqUrl);

  if (pathname !== config.rpc.service.routes.service) {
    return new Response("HTTP 404: Not Found", { status: 404 });
  }

  const reqBody = await request.json();
  const { method, params } = reqBody;

  switch (method) {
    // CASE -> method.create()
    case "create":
      try {
        const result = methods.create(params[0]);

        const createSuccess = {
          jsonrpc: "2.0",
          id: "request-id",
          result: [
            {
              success: true,
            },
          ],
        };

        return new Response(JSON.stringify(createSuccess), { status: 200 });
      } catch (error) {
        const createError = {
          jsonrpc: "2.0",
          id: "request-id",
          error: {
            code: -32000,
            // message: "Error message describing the nature of the error",
            message: error.message,
            data: "Optional data about the error",
          },
        };

        return new Response(JSON.stringify(createError), { status: 400 });
      }

    // CASE -> method.read()
    case "read":
      try {
        const result = methods.read(params[0]);

        const readSuccess = {
          jsonrpc: "2.0",
          id: "request-id",
          result: result,
        };

        return new Response(
          JSON.stringify(readSuccess),
          { status: 200 },
        );
      } catch (error) {
        const readError = {
          jsonrpc: "2.0",
          id: "request-id",
          error: {
            code: -32000,
            // message: "Error message describing the nature of the error",
            message: error.message,
            data: "Optional data about the error",
          },
        };

        return new Response(JSON.stringify(readError), { status: 400 });
      }

    // CASE -> method.list()
    case "list":
      try {
        const result = methods.list(params[0]);

        const listSuccess = {
          jsonrpc: "2.0",
          id: "request-id",
          result: result,
        };

        return new Response(JSON.stringify(listSuccess), { status: 200 });
      } catch (error) {
        const listError = {
          jsonrpc: "2.0",
          id: "request-id",
          error: {
            code: -32000,
            // message: "Error message describing the nature of the error",
            message: error.message,
            data: "Optional data about the error",
          },
        };

        return new Response(JSON.stringify(listError), { status: 400 });
      }

    // CASE -> method.update()
    case "update":
      try {
        const result = methods.update(params[0]);

        const updateSuccess = {
          jsonrpc: "2.0",
          id: "request-id",
          result: [
            {
              success: true,
            },
          ],
        };

        return new Response(JSON.stringify(updateSuccess), { status: 200 });
      } catch (error) {
        const updateError = {
          jsonrpc: "2.0",
          id: "request-id",
          error: {
            code: -32000,
            // message: "Error message describing the nature of the error",
            message: error.message,
            data: "Optional data about the error",
          },
        };

        return new Response(JSON.stringify(updateError), { status: 400 });
      }

    // CASE -> method.delete()
    case "delete":
      try {
        const result = methods.delete(params[0]);

        const deleteSuccess = {
          jsonrpc: "2.0",
          id: "request-id",
          result: result,
        };

        return new Response(JSON.stringify(deleteSuccess), { status: 200 });
      } catch (error) {
        const deleteError = {
          jsonrpc: "2.0",
          id: "request-id",
          error: {
            code: -32000,
            // message: "Error message describing the nature of the error",
            message: error.message,
            data: "Optional data about the error",
          },
        };

        return new Response(JSON.stringify(deleteError), { status: 400 });
      }

    default:
      // CASE -> method unknown
      const rpcMethodError = {
        jsonrpc: "2.0",
        id: "request-id",
        error: {
          code: -32000,
          // message: "Error message describing the nature of the error",
          message: "RPC Method not implemented.",
          data: "Optional data about the error",
        },
      };

      return new Response(JSON.stringify(rpcMethodError), { status: 501 });
  }
}

// Setup and start the Deno server
Deno.serve({
  port: config.rpc.service.port,
  hostname: config.rpc.service.hostname,
  onListen({ port, hostname }) {
    console.log(
      `%cDeno v${Deno.version.deno} : Typescript v${Deno.version.typescript} : V8 v${Deno.version.v8}
Application: Deno JSON RPCv2 Server based on OpenRPC specification
Permissions: --allow-net=${hostname}:${port}
Gateway URL: http://${hostname}:${port}
Root: http://${hostname}:${port}${config.rpc.service.routes.root}
Service: http://${hostname}:${port}${config.rpc.service.routes.service}
Status: http://${hostname}:${port}${config.rpc.service.routes.status}
Docs: http://${hostname}:${port}${config.rpc.service.routes.docs}`,
      "color: #7986cb",
    );
  },
  onError(error: unknown) {
    console.error(error);
    return new Response("HTTP 500: Internal Server Error", {
      status: 500,
      headers: { "content-type": "text/plain" },
    });
  },
}, handler);
json 打字稿 deno json-rpc

评论


答:

0赞 Neil Mayhew 9/14/2023 #1

最佳做法是将 200 状态代码用于由方法本身生成的所有错误,而不是由 http 服务器尝试调用该方法生成的错误。因此,例如,尝试创建已存在的资源将是 200 状态,而 HTTP 身份验证问题将是 403。

当然,如果你对代码进行结构化,以便将方法处理程序抽象到一个单独的层中,而处理程序则直接处理 http 的细节,这要容易得多。换言之,方法处理程序应该与传输无关,至少在概念上是这样。

以下是我找到的一些参考资料:

这两者的要点是,非 200 代码用于指示 HTTP 传输存在问题,而不是用于与方法调用的语义相关的错误。

下面是 JSON-RPC 网站“历史”部分的文档:

这与我上面概述的原则大致一致。

评论

1赞 suchislife 9/14/2023
男人。。。我想你只是通过我过度思考的头骨得到了这一点。传输层。每当我有任何关于偏离 200 响应的想法时,我都会问自己,错误是否发生在传输层?著名的。