我一直在开发一个 Deno 脚本,该脚本使用 Deno.serve() 实现 JSON-RPC v2 API,该脚本最近已稳定下来。虽然我已经设置了基本的 CRUD 操作和错误处理,但我不确定应该在不同场景中使用的 HTTP 状态代码。
上网查了一下,在JSON-RPC 2.0中,协议本身并没有严格定义具体HTTP状态码的使用。相反,它专注于 JSON 有效负载来传达成功和错误状态。然而,当 JSON-RPC 通过 HTTP 传输时,有一些常见的约定:
result
和 error
字段进行的。200 OK
和-32601 Method not found
错误进行通信),但如果 JSON-RPC 端点本身不存在,则可以使用此状态。找到了。我使用了以下状态代码:
有人可以澄清或提供关于哪些状态代码适合不同 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);
最佳实践是对方法本身生成的所有错误使用 200 状态代码,而不是 http 服务器尝试调用该方法生成的错误。因此,例如,尝试创建已存在的资源将是 200 状态,身份验证问题将是 403。
当然,如果您构建代码以便将方法处理程序抽象为与直接处理 http 细节的处理程序不同的单独层,那么这会容易得多。换句话说,方法处理程序应该是独立于传输的,至少在概念上是这样。
以下是我找到的一些参考资料:
这两者的要点是非 200 代码用于指示 HTTP 传输问题,而不是用于与方法调用语义相关的错误。
这是来自 JSON-RPC 网站“历史”部分的文档:
这与我上面概述的原则基本一致。