Skip to content

Commit 95024e6

Browse files
fix(mcp): the record resource answers a code-less throw as /api/v1/data does, so a sandboxed hook abort no longer names the hook (#31300)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X3apZSKwFDd6zvjM4gJPKAy)_ Fixes #31270 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:MCP 的记录资源(`objectstack://objects/{name}/records/{id}`)遇到不带错误码的异常时,改为与六个记录数据工具相同的读法(`/api/v1/data` 的 `mapDataError`):沙箱 hook 业务中止回 `HOOK_ABORTED` + 作者那句话 + 400;裸 `Error` 和沙箱崩溃(带不带码都一样)回 `500 INTERNAL_ERROR` `Internal server error`,原文只记服务端日志。 - 为什么改:之前资源对无码异常原样回 `{ error: 抛出的消息 }`,沙箱中止时就是 `hook 'NAME' threw: …`,把 hook 内部名交给客户端;故障原文也直接外泄。 - 风险与回滚:资源 JSON 的成员仍是 #23509 的 `{ error, code, status }`,只是无码异常现在也会带 `code`(门给出时)和 `status`;读资源的客户端会在无码异常上也看到这两个成员。只改 `@objectstack/mcp`,patch。回滚即还原 `bridgeResources` 的 catch。 - 先看:`packages/mcp/src/mcp-server-runtime.ts` 的 `bridgeResources` catch;`packages/mcp/src/mcp-coded-refusal.test.ts` 记录资源 describe 里的 #31270 块。 - 两分钟验证:`pnpm --filter @objectstack/mcp exec vitest run src/mcp-coded-refusal.test.ts -t 31270`,预期 5 个用例全部通过。 - 需要你做的:无,席位落地。 ## What changes The record resource's catch (`MCPServerRuntime.bridgeResources`, `packages/mcp/src/mcp-server-runtime.ts`) answered a throw with no `code` as `{ error: THROWN_MESSAGE }`. It now reads the throw the way the six record data tools have since #31218. That read is `dataDoorAnswerOf` in `mcp-http-tools.ts`, which asks the `/data` door's own `mapDataError`. Its one change is the `export` keyword (seat amendment). It is not on `packages/mcp/src/index.ts`, so nothing published moves, and there is no copy of the classification. A 5xx is logged once through `logServerFault` (`[5xx] 500 resources/read objectstack://objects/OBJ/records/ID — …`), the same line the data tools print with their own coordinates. The body keeps the #23509 members `{ error, code, status }`; only their values change on a code-less throw, and `code` and `status` can now appear there: | Throw (code-less unless stated) | Before | Now | `GET /api/v1/data/OBJ/ID` (`mapDataError`) | |---|---|---|---| | sandboxed hook abort (`SandboxError`, `innerMessage`) | `{ error: "hook 'demo_guard' threw: Error: Not enough leave" }` | `{ error: 'Not enough leave', code: 'HOOK_ABORTED', status: 400 }` | `400 HOOK_ABORTED` `Not enough leave` | | bare `Error('socket hang up')` | `{ error: 'socket hang up' }` | `{ error: 'Internal server error', code: 'INTERNAL_ERROR', status: 500 }`, logged once | `500 INTERNAL_ERROR` `Internal server error` | | sandboxed CRASH, no code | `{ error: "hook 'demo_guard' threw: TypeError: boom" }` | `{ error: 'Internal server error', code: 'INTERNAL_ERROR', status: 500 }`, logged once | same | | sandboxed CRASH with a declared code (the #31269 shape) | `{ error: "hook 'demo_guard' threw: TypeError: boom" }` (since #31269, = the code-less crash) | the same sanitised 500: this closes #31269's deviation on this face | same | | CONTROL: coded sandboxed refusal | `{ error: 'Locked.', code: 'RECORD_LOCKED', status: 409 }` | unchanged | `409 RECORD_LOCKED` `Locked.` | | CONTROL: coded non-sandbox refusal | its own sentence, code, status | unchanged | n/a | | `SQLITE_BUSY` (a coded driver string) | `{ error: 'SQLITE_BUSY: database is locked', code: 'SQLITE_BUSY' }` | unchanged: it rides the coded path | `500 DATABASE_ERROR` `Internal data error` | The `SQLITE_BUSY` row was measured as the order asked. This code-less fix does not settle it: it is a coded driver string, #31273's subject, and is left for that card. Every tool's answer is unchanged (measured before and after on the same probe of 5 throws × 13 tools). ## Tests `packages/mcp/src/mcp-coded-refusal.test.ts`, the record-resource describe. The old row "a code-less throw answers `{ error }` byte-identically" is replaced by a `#31270` describe: - undeclared sandboxed abort: `{ error: 'Not enough leave', code: 'HOOK_ABORTED', status: 400 }`, equal to `mapDataError`'s answer; no hook name, no `threw`; - bare `Error`: the exact sanitised 500 text, equal to the door's; one `[5xx] 500 resources/read objectstack://objects/task/records/t1 — ` log line carrying the thrown text; - sandboxed crash without and with a declared code: the exact sanitised 500; no hook name, no `TypeError`; - a thrown string and an empty `code`: the door's answer; - CONTROL: a coded sandboxed refusal keeps `Locked.` / `RECORD_LOCKED` / 409, a coded non-sandbox refusal keeps its own sentence byte for byte, and neither is logged. <details><summary>Red first and ablation</summary> Red first on `7c523aa1` (`main`, with PR #31290), new pins and no fix: `Tests 4 failed | 93 passed (97)`. The abort, bare `Error`, crash and string/empty-code rows failed; the CONTROL passed. With the fix: `Tests 97 passed (97)`. Ablation (`node scripts/ablation-replace.mjs`, `mcp-server-runtime.ts`): the code-less body `body = { error: door.message, ...(door.code !== undefined ? { code: door.code } : {}), status: door.status };` was replaced by one that answers the thrown message again. Output: `ok mutation landed: anchor 1 -> 0, blob d53fa45 -> 412ab95bb3d0`, then `Tests 4 failed | 93 passed (97)`, then `ok restored: blob == HEAD (d53fa45) and git diff HEAD is empty`. The tests import the package's source, so no `dist/` is involved. </details> ## Acceptance notes - `Clause-②: no`. The resource's members are the ones #23509 set, and the values change on a code-less throw: `code` and `status` now appear there. The changeset says so for resource readers. `dataDoorAnswerOf` is exported from its module only; `index.ts` re-exports `mcp-http-tools.js` by name and does not list it. - Not touched: the faces of #31271 (`run_action` / `resume_run`), #31272 (metadata tools, `list_actions`) and #31273 (driver string codes). ## Verification Base: `7c523aa1`; head: `e196ba77`. - `pnpm --filter @objectstack/mcp test`: `Test Files 60 passed (60)`, `Tests 826 passed (826)`, lock `VERDICT command-exit 0`. - `pnpm --filter @objectstack/mcp build` and `typecheck`: exit 0 (`check:test-typecheck`: 53 errors / 8 pinned signatures held). - `pnpm exec eslint --no-inline-config` on the three touched `.ts` files: exit 0 (`@objectstack/mcp` has no `lint` script). <details><summary>Derived gates (node scripts/pm/dispatch-gates.mjs --commands, 65 commands, all on e196ba7)</summary> - `node scripts/check-adr-0087-registration.mjs --base origin/main` :: exit 0 - `node scripts/check-adr-0087-registration.mjs --self-test` :: exit 0 - `node scripts/check-changeset-no-major.mjs --base origin/main` :: exit 0 - `node scripts/check-closing-keyword-parity.mjs` :: exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` :: exit 0 - `node scripts/check-comment-mask-adoption.mjs` :: exit 0 - `node scripts/check-comment-mask-adoption.mjs --self-test` :: exit 0 - `node scripts/check-comment-mask-corpus.mjs` :: exit 0 - `node scripts/check-dts-emitted.mjs --self-test` :: exit 0 - `node scripts/check-empty-changeset.mjs --base origin/main` :: exit 0 - `node scripts/check-empty-changeset.mjs --self-test` :: exit 0 - `node scripts/check-issue-citations.mjs` :: exit 0 - `node scripts/check-plugin-teardown-shape.mjs` :: exit 0 - `node scripts/check-plugin-teardown-shape.mjs --self-test` :: exit 0 - `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` :: exit 0 - `node scripts/ci/plan-test-core-shards.mjs --self-test` :: exit 0 - `node scripts/ci/prune-turbo-cache.mjs --self-test` :: exit 0 - `node scripts/ci/snapshot-next.mjs --self-test` :: exit 0 - `node scripts/docs-audit/check-affected-docs.mjs` :: exit 0 - `node scripts/docs-audit/check-drift-comment.mjs` :: exit 0 - `node scripts/pm/release-rehearsal-clone.mjs --self-test` :: exit 0 - `node scripts/release-pending-publish.mjs --self-test` :: exit 0 - `pnpm --filter @objectstack/spec run check:duration-unit-keys` :: exit 0 - `pnpm --filter @objectstack/spec run check:error-code-provenance` :: exit 0 - `pnpm check:adr-symbol-anchors` :: exit 0 - `pnpm check:changeset-gate-self-tests` :: exit 0 - `pnpm check:ci-filter-parity` :: exit 0 - `pnpm check:closed-ceremony-text` :: exit 0 - `pnpm check:cross-package-test-inputs` :: exit 0 - `pnpm check:dispatcher-error-vocabulary` :: exit 0 - `pnpm check:doc-authoring` :: exit 0 - `pnpm check:driver-memory-census` :: exit 0 - `pnpm check:dts-closure` :: exit 0 - `pnpm check:dual-build-cjs-loads` :: exit 0 - `pnpm check:engine-double-contract` :: exit 0 - `pnpm check:error-status-conformance` :: exit 0 - `pnpm check:gitlink-declared` :: exit 0 - `pnpm check:issue-citations` :: exit 0 - `pnpm check:keyed-text-bounds` :: exit 0 - `pnpm check:lean-entry-closure` :: exit 0 - `pnpm check:logger-receiver-detach` :: exit 0 - `pnpm check:nul-bytes` :: exit 0 - `pnpm check:object-def-param-keys` :: exit 0 - `pnpm check:objectql-double-limit` :: exit 0 - `pnpm check:optional-error-sink` :: exit 0 - `pnpm check:org-identifier` :: exit 0 - `pnpm check:page-declaration-shape` :: exit 0 - `pnpm check:platform-object-tenancy-census` :: exit 0 - `pnpm check:published-files` :: exit 0 - `pnpm check:query-options-erasure` :: exit 0 - `pnpm check:refd-timer-probe` :: exit 0 - `pnpm check:registry-log-declared` :: exit 0 - `pnpm check:rest-log-spy-declared` :: exit 0 - `pnpm check:settings-bind-window` :: exit 0 - `pnpm check:slot-lookup` :: exit 0 - `pnpm check:sourcemap-no-sources-content` :: exit 0 - `pnpm check:system-context-census` :: exit 0 - `pnpm check:test-source-alias` :: exit 0 - `pnpm check:tier-file-adoption` :: exit 0 - `pnpm check:type-check-coverage` :: exit 0 - `pnpm check:type-check-debt` :: exit 0 - `pnpm check:undeclared-dep-imports` :: exit 0 - `pnpm check:vouch-for-reference-callers` :: exit 0 - `pnpm check:watch-hint-literal` :: exit 0 - `pnpm check:where-matcher` :: exit 0 `node scripts/pm/dispatch-gates.mjs --ran`: `65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED`. `check:dual-build-cjs-loads` and `check:lean-entry-closure` first exited 3 (PREREQUISITE NOT MET: built entry points missing). Both exited 0 when re-run after `check:type-check-debt` had built every package. Not run locally (CI's): `test:repo`, the dogfood shards, the type-check lanes. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the touched files.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5oWCPWH6i2Lsts9cdP Co-authored-by: Claude <noreply@anthropic.com>
1 parent e669735 commit 95024e6

4 files changed

Lines changed: 132 additions & 21 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/mcp': patch
3+
---
4+
5+
fix(mcp): the record resource answers a code-less throw as `/api/v1/data` does, so a sandboxed hook abort no longer shows the hook's name
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`objectstack://objects/{name}/records/{id}` answered a thrown error that carried no `code` with `{ "error": <thrown message> }`. For a sandboxed hook body's `throw new Error('…')` that message is the runner's debug wrapper, `hook '<name>' threw: Error: …`, so a client saw the hook's internal name, and a fault's own text (a driver error, a crash) reached it verbatim. `GET /api/v1/data/{name}/{id}` answers the same throws `400 HOOK_ABORTED` with the body's sentence, or `500 INTERNAL_ERROR` `Internal server error`.
11+
12+
The resource now answers a code-less throw with the `/data` door's answer, read the way the record data tools read it since the code-less fix for those tools (`mapDataError`, `@objectstack/types`), in the members the resource already uses: `{ "error", "code", "status" }`. A resource reader should expect `code` and `status` on a code-less throw too now. `status` is always set, and `code` is set whenever the door names one:
13+
14+
- an undeclared sandboxed hook abort: `{ "error": "<the body's sentence>", "code": "HOOK_ABORTED", "status": 400 }`;
15+
- a bare `Error`, or a sandboxed body that crashed (with or without a declared code): `{ "error": "Internal server error", "code": "INTERNAL_ERROR", "status": 500 }`. The thrown text is logged once on the server as a `[5xx]` line instead of being sent.
16+
17+
A coded refusal is answered as before.

‎packages/mcp/src/mcp-coded-refusal.test.ts‎

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -564,12 +564,84 @@ describe('the record resource: a coded refusal adds its code beside the unchange
564564
expect(coded).not.toContain('RECORD_LOCKED');
565565
});
566566

567-
it('a code-less throw answers `{ error }` byte-identically', async () => {
568-
expect(await readRefused(new Error('engine exploded'))).toBe(JSON.stringify({ error: 'engine exploded' }));
569-
expect(await readRefused('a thrown string')).toBe(JSON.stringify({ error: 'a thrown string' }));
570-
expect(await readRefused(Object.assign(new Error('empty code'), { code: '' }))).toBe(
571-
JSON.stringify({ error: 'empty code' }),
567+
/**
568+
* [#31270] A code-less throw answers what the record data tools answer since
569+
* #31218: the `/api/v1/data` door's answer (`mapDataError`, the GET by-id
570+
* door), in this resource's `{ error, code, status }` members. It used to
571+
* answer `{ error }` with the thrown message, so a sandboxed hook's abort
572+
* reached the client as the runner's wrapper naming the hook and a fault's
573+
* own text reached it verbatim.
574+
*/
575+
describe('a code-less throw answers the /data door\'s answer (#31270)', () => {
576+
afterEach(() => {
577+
vi.restoreAllMocks();
578+
});
579+
580+
/** The `SandboxError` the QuickJS runner raises for a hook body's throw (measured on the real runner). */
581+
const sandboxed = (inner: string, extra: Record<string, unknown> = {}) => Object.assign(
582+
new Error(`hook 'demo_resource_guard' threw: ${inner}`),
583+
{ name: 'SandboxError', innerMessage: inner.replace(/^Error: /, '') },
584+
extra,
572585
);
586+
587+
/** The door's answer to `thrown`, in this resource's member order. */
588+
function doorBody(thrown: unknown): Record<string, unknown> {
589+
const door = mapDataError(thrown, 'task');
590+
return {
591+
error: door.body.error,
592+
...(typeof door.body.code === 'string' ? { code: door.body.code } : {}),
593+
status: door.status,
594+
};
595+
}
596+
597+
it('an undeclared sandboxed hook abort: HOOK_ABORTED, the inner sentence, 400; no hook name', async () => {
598+
const thrown = sandboxed('Error: Not enough leave');
599+
const text = await readRefused(thrown);
600+
expect(JSON.parse(text)).toEqual({ error: 'Not enough leave', code: 'HOOK_ABORTED', status: 400 });
601+
expect(JSON.parse(text)).toEqual(doorBody(thrown));
602+
expect(text).not.toContain('demo_resource_guard');
603+
expect(text).not.toContain('threw');
604+
});
605+
606+
it('a bare Error: the sanitised 500, its message withheld and logged once for the operator', async () => {
607+
const log = vi.spyOn(console, 'error').mockImplementation(() => {});
608+
const thrown = new Error('socket hang up');
609+
const text = await readRefused(thrown);
610+
expect(text).toBe(JSON.stringify({ error: 'Internal server error', code: 'INTERNAL_ERROR', status: 500 }));
611+
expect(JSON.parse(text)).toEqual(doorBody(thrown));
612+
expect(log).toHaveBeenCalledTimes(1);
613+
const line = String(log.mock.calls[0]![0]);
614+
expect(line).toMatch(/^\[5xx\] 500 resources\/read objectstack:\/\/objects\/task\/records\/t1 — /);
615+
expect(line).toContain('socket hang up');
616+
});
617+
618+
it('a sandboxed body that CRASHED, with or without a declared code: the sanitised 500, no hook name, no native text', async () => {
619+
vi.spyOn(console, 'error').mockImplementation(() => {});
620+
for (const thrown of [sandboxed('TypeError: boom'), sandboxed('TypeError: boom', { code: 'RECORD_LOCKED', status: 409 })]) {
621+
const text = await readRefused(thrown);
622+
expect(text, thrown.message).toBe(JSON.stringify({ error: 'Internal server error', code: 'INTERNAL_ERROR', status: 500 }));
623+
expect(text).not.toContain('demo_resource_guard');
624+
expect(text).not.toContain('TypeError');
625+
}
626+
});
627+
628+
it('a thrown string and an empty `code` are no code: the door\'s answer', async () => {
629+
vi.spyOn(console, 'error').mockImplementation(() => {});
630+
for (const thrown of ['a thrown string', Object.assign(new Error('empty code'), { code: '' })]) {
631+
expect(JSON.parse(await readRefused(thrown)), String(thrown)).toEqual(doorBody(thrown));
632+
}
633+
});
634+
635+
it('CONTROL: a coded sandboxed refusal keeps the #31246 answer, and a coded non-sandbox refusal its own; neither is logged', async () => {
636+
const log = vi.spyOn(console, 'error').mockImplementation(() => {});
637+
expect(JSON.parse(await readRefused(sandboxed('Error: Locked.', { code: 'RECORD_LOCKED', status: 409 })))).toEqual(
638+
{ error: 'Locked.', code: 'RECORD_LOCKED', status: 409 },
639+
);
640+
expect(await readRefused(Object.assign(new Error('The period is closed.'), { code: 'RECORD_LOCKED', status: 409 }))).toBe(
641+
JSON.stringify({ error: 'The period is closed.', code: 'RECORD_LOCKED', status: 409 }),
642+
);
643+
expect(log).not.toHaveBeenCalled();
644+
});
573645
});
574646
});
575647

‎packages/mcp/src/mcp-http-tools.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,11 @@ interface DataDoorAnswer {
535535
* wrapper, and `Internal server error` for a fault. `object`, `field` and the
536536
* other context keys the door adds are not read, as `details` is not
537537
* ({@link errorResultWithCode}).
538+
*
539+
* [#31270] Exported for the record resource (`MCPServerRuntime.bridgeResources`),
540+
* which answers a code-less throw with the same read; not on the package entry.
538541
*/
539-
function dataDoorAnswerOf(err: unknown, objectName: string): DataDoorAnswer {
542+
export function dataDoorAnswerOf(err: unknown, objectName: string): DataDoorAnswer {
540543
const door = mapDataError(err, objectName);
541544
const code = typeof door.body.code === 'string' && door.body.code !== '' ? door.body.code : undefined;
542545
const message = typeof door.body.error === 'string' && door.body.error !== '' ? door.body.error : messageOf(err);

‎packages/mcp/src/mcp-server-runtime.ts‎

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import type {
1414
ToolRegistry,
1515
ToolExecutionResult,
1616
} from './types.js';
17-
import { wireBridgeTools, grantsAuthoringTools, thrownRefusalEnvelope } from './mcp-http-tools.js';
17+
import { wireBridgeTools, grantsAuthoringTools, thrownRefusalEnvelope, dataDoorAnswerOf } from './mcp-http-tools.js';
18+
import { isServerFault, logServerFault } from '@objectstack/types';
1819
import type {
1920
McpDataBridge,
2021
McpActionBridge,
@@ -1555,35 +1556,53 @@ export class MCPServerRuntime {
15551556
}],
15561557
};
15571558
} catch (err) {
1558-
const message = err instanceof Error ? err.message : String(err);
15591559
// [#23509] A coded refusal carries its ADR-0112 `code` (and its
15601560
// `status`, when it has one) beside the sentence, so a client reads
15611561
// the code it branches on. A held owner's key is refused here "with
15621562
// the same code" (ruling C′ item 1) as on REST. The members are
15631563
// ADDED to the body, in the `{ error, code, status }` vocabulary
15641564
// the two sibling resources above already answer with (#6055,
1565-
// #6504). `error` stays the same string, so a client that tells a
1566-
// refusal from a row by `typeof body.error === 'string'` still
1567-
// does. A throw with no code answers `{ error }` as before.
1565+
// #6504). `error` stays a string, so a client that tells a refusal
1566+
// from a row by `typeof body.error === 'string'` still does.
15681567
// `details` is withheld, as on the data tools; `errorResultWithCode`
15691568
// in mcp-http-tools.ts says why.
15701569
// [#31246] A coded refusal's `error` is the envelope's sentence,
15711570
// so a sandboxed hook's refusal answers the body's own sentence
15721571
// rather than the runner's wrapper naming the hook, as the tools do.
1572+
// [#31270] A throw with NO code answers what the record data tools
1573+
// answer since #31218: the `/api/v1/data` door's answer
1574+
// (`dataDoorAnswerOf`, the door's own `mapDataError`), in the same
1575+
// members, so `code` and `status` now appear on a code-less throw
1576+
// too. A sandboxed hook's abort is `HOOK_ABORTED` with the inner
1577+
// sentence; a fault (a bare `Error`, a crash) is the sanitised 500,
1578+
// its thrown text withheld from the client and logged once, as the
1579+
// door logs it.
1580+
const uri = `objectstack://objects/${objectName}/records/${recordId}`;
15731581
const envelope = thrownRefusalEnvelope(err);
1582+
let body: { error: string; code?: string; status?: number };
1583+
if (envelope) {
1584+
body = {
1585+
error: envelope.message,
1586+
code: envelope.code,
1587+
...(envelope.status !== undefined ? { status: envelope.status } : {}),
1588+
};
1589+
} else {
1590+
const door = dataDoorAnswerOf(err, objectName);
1591+
if (isServerFault(door.status)) {
1592+
logServerFault({
1593+
status: door.status,
1594+
error: err,
1595+
...(door.code !== undefined ? { code: door.code } : {}),
1596+
request: { method: 'resources/read', path: uri },
1597+
});
1598+
}
1599+
body = { error: door.message, ...(door.code !== undefined ? { code: door.code } : {}), status: door.status };
1600+
}
15741601
return {
15751602
contents: [{
1576-
uri: `objectstack://objects/${objectName}/records/${recordId}`,
1603+
uri,
15771604
mimeType: 'application/json',
1578-
text: JSON.stringify(
1579-
envelope
1580-
? {
1581-
error: envelope.message,
1582-
code: envelope.code,
1583-
...(envelope.status !== undefined ? { status: envelope.status } : {}),
1584-
}
1585-
: { error: message },
1586-
),
1605+
text: JSON.stringify(body),
15871606
}],
15881607
};
15891608
}

0 commit comments

Comments
 (0)