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 <cursoragent@cursor.com>
This commit is contained in:
@@ -27,17 +27,32 @@ function buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath
|
|||||||
return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`;
|
return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseRemoteOperationResponse(response, bindingKey, method) {
|
function throwRemoteOperationError(response, bindingKey, method, bodyText, payload) {
|
||||||
if (!response.ok) {
|
const error = new Error(
|
||||||
const bodyText = await response.text().catch(() => '');
|
payload?.message ??
|
||||||
throw new Error(
|
|
||||||
`MindSpace remote adapter ${bindingKey}.${method}() failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`,
|
`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;
|
if (response.status === 204) return null;
|
||||||
const text = await response.text();
|
if (!bodyText) return null;
|
||||||
if (!text) return null;
|
return payload ?? JSON.parse(bodyText);
|
||||||
return JSON.parse(text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invokeRemoteOperation({
|
async function invokeRemoteOperation({
|
||||||
|
|||||||
@@ -133,3 +133,26 @@ test('createMindSpaceRemoteServerAdapter surfaces remote conversation package er
|
|||||||
/conversationPackageRegistry\.readManifestForSession\(\) failed with 502: upstream manifest missing/,
|
/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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -53,6 +53,36 @@ function json(res, statusCode, payload) {
|
|||||||
res.end(body);
|
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({
|
export async function createMindSpaceRpcRequestHandler({
|
||||||
adapter,
|
adapter,
|
||||||
env = process.env,
|
env = process.env,
|
||||||
@@ -113,10 +143,11 @@ export async function createMindSpaceRpcRequestHandler({
|
|||||||
const result = await service[method](...args);
|
const result = await service[method](...args);
|
||||||
return json(res, 200, result);
|
return json(res, 200, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error?.('[MindSpace RPC Error]', error);
|
const statusCode = resolveRpcErrorStatus(error);
|
||||||
return json(res, 500, {
|
if (statusCode >= 500) {
|
||||||
message: error instanceof Error ? error.message : String(error),
|
logger.error?.('[MindSpace RPC Error]', error);
|
||||||
});
|
}
|
||||||
|
return json(res, statusCode, serializeRpcError(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,3 +196,52 @@ test('rpc invocation revives JSON-serialized buffers before dispatch', async ()
|
|||||||
assert.deepEqual(response.body, { isBuffer: true, size: payload.length });
|
assert.deepEqual(response.body, { isBuffer: true, size: payload.length });
|
||||||
assert.equal(Buffer.isBuffer(adapter.calls.writeUploadContent[0][2]), true);
|
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');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user