From 53eb0014bace2025341e91f723301ff37e6a10b5 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 5 Jul 2026 19:44:58 +0800 Subject: [PATCH] fix(mindspace): map publication RPC errors to HTTP status codes Return 404/401/403 from MindSpace RPC for known publication errors and preserve error codes through the Portal remote adapter. Co-authored-by: Cursor --- mindspace-remote-server-adapter.mjs | 31 +++++++++--- mindspace-remote-server-adapter.test.mjs | 23 +++++++++ mindspace-service/mindspace-rpc-server.mjs | 39 +++++++++++++-- .../mindspace-rpc-server.test.mjs | 49 +++++++++++++++++++ 4 files changed, 130 insertions(+), 12 deletions(-) diff --git a/mindspace-remote-server-adapter.mjs b/mindspace-remote-server-adapter.mjs index 09c0fbf..92aa6f6 100644 --- a/mindspace-remote-server-adapter.mjs +++ b/mindspace-remote-server-adapter.mjs @@ -27,17 +27,32 @@ function buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`; } -async function parseRemoteOperationResponse(response, bindingKey, method) { - if (!response.ok) { - const bodyText = await response.text().catch(() => ''); - throw new Error( +function throwRemoteOperationError(response, bindingKey, method, bodyText, payload) { + const error = new Error( + payload?.message ?? `MindSpace remote adapter ${bindingKey}.${method}() failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`, - ); + ); + if (payload?.code) error.code = payload.code; + if (payload?.details !== undefined) error.details = payload.details; + throw error; +} + +async function parseRemoteOperationResponse(response, bindingKey, method) { + const bodyText = await response.text().catch(() => ''); + let payload = null; + if (bodyText) { + try { + payload = JSON.parse(bodyText); + } catch { + payload = null; + } + } + if (!response.ok) { + throwRemoteOperationError(response, bindingKey, method, bodyText, payload); } if (response.status === 204) return null; - const text = await response.text(); - if (!text) return null; - return JSON.parse(text); + if (!bodyText) return null; + return payload ?? JSON.parse(bodyText); } async function invokeRemoteOperation({ diff --git a/mindspace-remote-server-adapter.test.mjs b/mindspace-remote-server-adapter.test.mjs index 9105074..1aa6c51 100644 --- a/mindspace-remote-server-adapter.test.mjs +++ b/mindspace-remote-server-adapter.test.mjs @@ -133,3 +133,26 @@ test('createMindSpaceRemoteServerAdapter surfaces remote conversation package er /conversationPackageRegistry\.readManifestForSession\(\) failed with 502: upstream manifest missing/, ); }); + +test('createMindSpaceRemoteServerAdapter preserves publication error codes from rpc payload', async () => { + const adapter = createMindSpaceRemoteServerAdapter({ + endpoint: 'https://mindspace.example.com/', + authToken: 'secret-token', + fetchFn: async () => ({ + ok: false, + status: 404, + async text() { + return JSON.stringify({ + message: '公开页面不存在', + code: 'publication_not_found', + }); + }, + }), + logger: { log() {}, warn() {}, error() {} }, + }); + + await assert.rejects( + () => adapter.publicationService.resolvePublic('john', 'missing-page', null, null, {}), + (error) => error instanceof Error && error.code === 'publication_not_found', + ); +}); diff --git a/mindspace-service/mindspace-rpc-server.mjs b/mindspace-service/mindspace-rpc-server.mjs index b3eb6ae..5968bac 100644 --- a/mindspace-service/mindspace-rpc-server.mjs +++ b/mindspace-service/mindspace-rpc-server.mjs @@ -53,6 +53,36 @@ function json(res, statusCode, payload) { res.end(body); } +function serializeRpcError(error) { + return { + message: error instanceof Error ? error.message : String(error), + code: error?.code ?? 'internal_error', + ...(error?.details !== undefined ? { details: error.details } : {}), + }; +} + +function resolveRpcErrorStatus(error) { + switch (error?.code) { + case 'publication_not_found': + case 'publication_owner_not_found': + case 'page_not_found': + case 'category_not_found': + return 404; + case 'publication_login_required': + return 401; + case 'publication_password_required': + return 403; + case 'invalid_input': + case 'invalid_publish_input': + case 'invalid_state_transition': + case 'slug_conflict': + case 'security_ack_required': + return 400; + default: + return 500; + } +} + export async function createMindSpaceRpcRequestHandler({ adapter, env = process.env, @@ -113,10 +143,11 @@ export async function createMindSpaceRpcRequestHandler({ const result = await service[method](...args); return json(res, 200, result); } catch (error) { - logger.error?.('[MindSpace RPC Error]', error); - return json(res, 500, { - message: error instanceof Error ? error.message : String(error), - }); + const statusCode = resolveRpcErrorStatus(error); + if (statusCode >= 500) { + logger.error?.('[MindSpace RPC Error]', error); + } + return json(res, statusCode, serializeRpcError(error)); } }; } diff --git a/mindspace-service/mindspace-rpc-server.test.mjs b/mindspace-service/mindspace-rpc-server.test.mjs index 6b5bf95..af05657 100644 --- a/mindspace-service/mindspace-rpc-server.test.mjs +++ b/mindspace-service/mindspace-rpc-server.test.mjs @@ -196,3 +196,52 @@ test('rpc invocation revives JSON-serialized buffers before dispatch', async () assert.deepEqual(response.body, { isBuffer: true, size: payload.length }); assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true); }); + +test('rpc maps publication_not_found to 404 with error code', async () => { + const adapter = createStubAdapter(); + adapter.publicationService.resolvePublic = async () => { + const error = new Error('公开页面不存在'); + error.code = 'publication_not_found'; + throw error; + }; + const handler = await createMindSpaceRpcRequestHandler({ + adapter, + env: { + MINDSPACE_MEMIND_ROOT: '..', + }, + }); + + const response = await runRequest(handler, { + method: 'POST', + path: '/mindspace/v1/adapter/publicationService/resolvePublic', + body: JSON.stringify({ args: ['john', 'missing-page', null, null, {}] }), + }); + + assert.equal(response.statusCode, 404); + assert.equal(response.body.code, 'publication_not_found'); + assert.match(response.body.message, /公开页面不存在/); +}); + +test('rpc maps publication_login_required to 401 with error code', async () => { + const adapter = createStubAdapter(); + adapter.publicationService.resolvePublic = async () => { + const error = new Error('登录后才能访问此页面'); + error.code = 'publication_login_required'; + throw error; + }; + const handler = await createMindSpaceRpcRequestHandler({ + adapter, + env: { + MINDSPACE_MEMIND_ROOT: '..', + }, + }); + + const response = await runRequest(handler, { + method: 'POST', + path: '/mindspace/v1/adapter/publicationService/resolvePublic', + body: JSON.stringify({ args: ['john', 'login-page', null, null, {}] }), + }); + + assert.equal(response.statusCode, 401); + assert.equal(response.body.code, 'publication_login_required'); +});