mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
feat(kap-server): surface originating stack trace on error envelopes
- add optional `stack` field to errEnvelope and the envelope schema/interface; omitted when undefined so the wire shape stays byte-identical for callers without a stack - thread `err.stack` through route error mappers plus the global and transport error handlers - preserve `details` on `session.undo_unavailable` while adding its stack - update tests to assert stacks are surfaced, reversing the prior no-leak contract
This commit is contained in:
parent
bf976f470c
commit
b51d77527e
16 changed files with 98 additions and 47 deletions
|
|
@ -45,6 +45,7 @@ export function installErrorHandler(app: ErrorHandlerHost): void {
|
|||
ErrorCode.INTERNAL_ERROR,
|
||||
err.message !== undefined && err.message !== '' ? err.message : 'internal error',
|
||||
requestId,
|
||||
err.stack,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -483,34 +483,34 @@ function sendMappedError(reply: Reply, requestId: string, err: unknown): void {
|
|||
if (isKimiError(err)) {
|
||||
switch (err.code) {
|
||||
case ErrorCodes.FS_PATH_ESCAPES:
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_PATH_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_IS_DIRECTORY:
|
||||
reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_IS_DIRECTORY, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_ALREADY_EXISTS:
|
||||
reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_ALREADY_EXISTS, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_IS_BINARY:
|
||||
reply.send(errEnvelope(ErrorCode.FS_IS_BINARY, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_IS_BINARY, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_TOO_LARGE:
|
||||
reply.send(errEnvelope(ErrorCode.FS_TOO_LARGE, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_TOO_LARGE, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_TOO_MANY_RESULTS:
|
||||
reply.send(errEnvelope(ErrorCode.FS_TOO_MANY_RESULTS, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_TOO_MANY_RESULTS, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_GREP_TIMEOUT:
|
||||
reply.send(errEnvelope(ErrorCode.FS_GREP_TIMEOUT, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_GREP_TIMEOUT, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.FS_GIT_UNAVAILABLE:
|
||||
reply.send(errEnvelope(ErrorCode.FS_GIT_UNAVAILABLE, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_GIT_UNAVAILABLE, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.SESSION_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -519,6 +519,7 @@ function sendMappedError(reply: Reply, requestId: string, err: unknown): void {
|
|||
ErrorCode.INTERNAL_ERROR,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
requestId,
|
||||
err instanceof Error ? err.stack : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,14 +158,14 @@ function sendMappedError(
|
|||
if (isKimiError(err)) {
|
||||
switch (err.code) {
|
||||
case 'session.not_found':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'message.not_found':
|
||||
reply.send(errEnvelope(ErrorCode.MESSAGE_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.MESSAGE_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
}
|
||||
reply.send(
|
||||
errEnvelope(ErrorCode.INTERNAL_ERROR, err instanceof Error ? err.message : String(err), requestId),
|
||||
errEnvelope(ErrorCode.INTERNAL_ERROR, err instanceof Error ? err.message : String(err), requestId, err instanceof Error ? err.stack : undefined),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,11 +290,11 @@ function sendMappedError(
|
|||
): boolean {
|
||||
if (!isKimiError(err)) return false;
|
||||
if (err.code === 'provider.not_found') {
|
||||
reply.send(errEnvelope(ErrorCode.PROVIDER_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.PROVIDER_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return true;
|
||||
}
|
||||
if (err.code === 'model.not_found') {
|
||||
reply.send(errEnvelope(ErrorCode.MODEL_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.MODEL_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -221,13 +221,13 @@ function sendMappedError(
|
|||
switch (err.code) {
|
||||
case 'session.not_found':
|
||||
case 'agent.not_found':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'prompt.not_found':
|
||||
reply.send(errEnvelope(ErrorCode.PROMPT_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.PROMPT_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'session.busy':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_BUSY, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_BUSY, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'prompt.already_completed':
|
||||
reply.send({
|
||||
|
|
@ -235,11 +235,12 @@ function sendMappedError(
|
|||
msg: err.message,
|
||||
data: { aborted: false },
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
});
|
||||
return;
|
||||
case 'request.invalid':
|
||||
case 'validation.failed':
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'auth.provisioning_required':
|
||||
reply.send({
|
||||
|
|
@ -247,6 +248,7 @@ function sendMappedError(
|
|||
msg: err.message,
|
||||
data: null,
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
details: null,
|
||||
});
|
||||
return;
|
||||
|
|
@ -267,6 +269,7 @@ function sendMappedError(
|
|||
msg: err.message,
|
||||
data: null,
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
details,
|
||||
});
|
||||
return;
|
||||
|
|
@ -288,6 +291,7 @@ function sendMappedError(
|
|||
msg: err.message,
|
||||
data: null,
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
details,
|
||||
});
|
||||
return;
|
||||
|
|
@ -298,6 +302,7 @@ function sendMappedError(
|
|||
msg: err.message,
|
||||
data: null,
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
details: authModelDetails(err),
|
||||
});
|
||||
return;
|
||||
|
|
@ -308,6 +313,7 @@ function sendMappedError(
|
|||
ErrorCode.INTERNAL_ERROR,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
requestId,
|
||||
err instanceof Error ? err.stack : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -913,38 +913,44 @@ function sendMappedError(
|
|||
switch (err.code) {
|
||||
case 'session.not_found':
|
||||
case 'agent.not_found':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'session.fork_active_turn':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_BUSY, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_BUSY, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'compaction.unable':
|
||||
reply.send(errEnvelope(ErrorCode.COMPACTION_UNABLE, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.COMPACTION_UNABLE, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'session.undo_unavailable':
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_UNDO_UNAVAILABLE, err.message, requestId));
|
||||
reply.send({
|
||||
code: ErrorCode.SESSION_UNDO_UNAVAILABLE,
|
||||
msg: err.message,
|
||||
data: (err as { details?: unknown }).details ?? null,
|
||||
request_id: requestId,
|
||||
stack: err.stack,
|
||||
});
|
||||
return;
|
||||
case ErrorCodes.GOAL_ALREADY_EXISTS:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_ALREADY_EXISTS, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_ALREADY_EXISTS, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.GOAL_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.GOAL_STATUS_INVALID:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_STATUS_INVALID, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_STATUS_INVALID, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.GOAL_NOT_RESUMABLE:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_NOT_RESUMABLE, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_NOT_RESUMABLE, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.GOAL_OBJECTIVE_EMPTY:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_OBJECTIVE_EMPTY, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_OBJECTIVE_EMPTY, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.GOAL_OBJECTIVE_TOO_LONG:
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_OBJECTIVE_TOO_LONG, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.GOAL_OBJECTIVE_TOO_LONG, err.message, requestId, err.stack));
|
||||
return;
|
||||
case 'request.invalid':
|
||||
case 'validation.failed':
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -953,6 +959,7 @@ function sendMappedError(
|
|||
ErrorCode.INTERNAL_ERROR,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
requestId,
|
||||
err instanceof Error ? err.stack : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -375,10 +375,10 @@ function sendMappedError(
|
|||
switch (err.code) {
|
||||
case ErrorCodes.SKILL_NOT_FOUND:
|
||||
case ErrorCodes.SKILL_NAME_EMPTY:
|
||||
reply.send(errEnvelope(ErrorCode.SKILL_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SKILL_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.SKILL_TYPE_UNSUPPORTED:
|
||||
reply.send(errEnvelope(ErrorCode.SKILL_NOT_ACTIVATABLE, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SKILL_NOT_ACTIVATABLE, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,14 +107,14 @@ export function registerSnapshotRoutes(app: SnapshotRouteHost, deps: SnapshotRou
|
|||
reply.send(okEnvelope(data, req.id));
|
||||
} catch (err) {
|
||||
if (err instanceof SnapshotNotFoundError) {
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, req.id));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, req.id, err.stack));
|
||||
return;
|
||||
}
|
||||
if (err instanceof SnapshotTimeoutError) {
|
||||
core.accessor
|
||||
.get(ILogService)
|
||||
.warn('snapshot.timeout', { sid: session_id, duration_ms: err.timeoutMs });
|
||||
reply.send(errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, req.id));
|
||||
reply.send(errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, req.id, err.stack));
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -232,10 +232,10 @@ function sendMappedError(
|
|||
if (isKimiError(err)) {
|
||||
switch (err.code) {
|
||||
case ErrorCodes.SESSION_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
case ErrorCodes.TERMINAL_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.TERMINAL_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.TERMINAL_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -244,7 +244,7 @@ function sendMappedError(
|
|||
// escapes. TODO: push a coded error into `assertAllowed` so this branch can
|
||||
// be folded into the `isKimiError` switch above.
|
||||
if (err instanceof Error && err.message.startsWith('Path outside workspace')) {
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_ESCAPES_SESSION, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ function sendMappedError(
|
|||
err: unknown,
|
||||
): void {
|
||||
if (err instanceof KimiError && err.code === ErrorCodes.MCP_SERVER_NOT_FOUND) {
|
||||
reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.MCP_SERVER_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -113,15 +113,15 @@ function sendMappedError(
|
|||
err: unknown,
|
||||
): void {
|
||||
if (err instanceof HostFolderNotAbsoluteError) {
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.VALIDATION_FAILED, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
if (err instanceof HostFolderNotFoundError) {
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_PATH_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
if (err instanceof HostFolderPermissionError) {
|
||||
reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId));
|
||||
reply.send(errEnvelope(ErrorCode.FS_PERMISSION_DENIED, err.message, requestId, err.stack));
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
|
|
|||
|
|
@ -50,15 +50,16 @@ const KIMI_TO_PROTOCOL: Record<string, ErrorCode> = {
|
|||
export function mapError(err: unknown, requestId: string): ReturnType<typeof errEnvelope> {
|
||||
if (err instanceof KimiError) {
|
||||
const code = KIMI_TO_PROTOCOL[err.code] ?? ErrorCode.INTERNAL_ERROR;
|
||||
return errEnvelope(code, err.message, requestId);
|
||||
return errEnvelope(code, err.message, requestId, err.stack);
|
||||
}
|
||||
if (err instanceof TimeoutError) {
|
||||
return errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, requestId);
|
||||
return errEnvelope(ErrorCode.INTERNAL_ERROR, err.message, requestId, err.stack);
|
||||
}
|
||||
return errEnvelope(
|
||||
ErrorCode.INTERNAL_ERROR,
|
||||
err instanceof Error ? err.message : String(err),
|
||||
requestId,
|
||||
err instanceof Error ? err.stack : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -499,9 +499,13 @@ describe('server-v2 /api/v2 RPC', () => {
|
|||
expect(code).not.toBe(0);
|
||||
});
|
||||
|
||||
it('does not leak stack traces on error', async () => {
|
||||
it('surfaces the originating stack trace on error', async () => {
|
||||
const { body } = await call<null>('POST', '/api/v2/session/nope/session:read');
|
||||
expect(JSON.stringify(body)).not.toContain('stack');
|
||||
// Contract: error envelopes carry the thrown error's stack so operators can
|
||||
// locate the source (the 40401 below originates in `dispatch`).
|
||||
const json = JSON.stringify(body);
|
||||
expect(json).toContain('"stack"');
|
||||
expect(json).toContain('dispatch');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ interface Envelope<T> {
|
|||
data: T;
|
||||
request_id: string;
|
||||
details?: { path: string; message: string }[];
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
interface SessionWire {
|
||||
|
|
@ -339,6 +340,8 @@ describe('server-v2 /api/v1/sessions', () => {
|
|||
// (40911), not the pre-fix "session does not exist" (40401).
|
||||
expect(res.body.code).toBe(40911);
|
||||
expect(res.body.msg).toMatch(/nothing to undo/i);
|
||||
// The thrown KimiError's stack is surfaced so operators can locate the source.
|
||||
expect(res.body.stack).toEqual(expect.stringContaining('sessionLegacyService'));
|
||||
});
|
||||
|
||||
it('rejects an unsupported action suffix (40001)', async () => {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ describe('envelope', () => {
|
|||
'{"code":40001,"msg":"validation failed","data":null,"request_id":"req_z"}',
|
||||
);
|
||||
});
|
||||
|
||||
it('errEnvelope surfaces stack when provided and omits it when absent', () => {
|
||||
const err = new Error('boom');
|
||||
const withStack = errEnvelope(ErrorCode.INTERNAL_ERROR, 'boom', 'req_s', err.stack);
|
||||
expect(withStack.stack).toBe(err.stack);
|
||||
expect(JSON.stringify(withStack)).toContain('"stack":');
|
||||
expect(envelopeSchema(z.any()).parse(withStack).stack).toBe(err.stack);
|
||||
|
||||
// No stack → field is absent and the wire shape is byte-identical to before.
|
||||
const without = errEnvelope(ErrorCode.INTERNAL_ERROR, 'boom', 'req_s');
|
||||
expect(JSON.stringify(without)).toBe(
|
||||
'{"code":50001,"msg":"boom","data":null,"request_id":"req_s"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error-codes', () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export const envelopeSchema = <T extends z.ZodTypeAny>(data: T) =>
|
|||
data: data.nullable(),
|
||||
request_id: z.string(),
|
||||
details: z.unknown().optional(),
|
||||
stack: z.string().optional(),
|
||||
});
|
||||
|
||||
export interface Envelope<T> {
|
||||
|
|
@ -15,12 +16,25 @@ export interface Envelope<T> {
|
|||
data: T | null;
|
||||
request_id: string;
|
||||
details?: unknown;
|
||||
stack?: string;
|
||||
}
|
||||
|
||||
export function okEnvelope<T>(data: T, requestId: string): Envelope<T> {
|
||||
return { code: 0, msg: 'success', data, request_id: requestId };
|
||||
}
|
||||
|
||||
export function errEnvelope(code: number, msg: string, requestId: string): Envelope<null> {
|
||||
return { code, msg, data: null, request_id: requestId };
|
||||
/**
|
||||
* Build an error envelope. When `stack` is provided it is surfaced verbatim on
|
||||
* the wire so operators can see where a thrown error originated; when omitted
|
||||
* (or `undefined`) the field is absent and the wire shape stays byte-identical
|
||||
* to the original `{ code, msg, data: null, request_id }` — `JSON.stringify`
|
||||
* drops `undefined` properties, so callers that have no stack are unaffected.
|
||||
*/
|
||||
export function errEnvelope(
|
||||
code: number,
|
||||
msg: string,
|
||||
requestId: string,
|
||||
stack?: string,
|
||||
): Envelope<null> {
|
||||
return { code, msg, data: null, request_id: requestId, stack };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue