fix(core): include success field in API responses#2098
Conversation
The REST API reference documents a discriminated response envelope
(`{ success: true, data }` on success, `{ success: false, error }` on
error), but apiSuccess/apiError and the generated OpenAPI schema both
omitted the `success` field, so clients could not rely on the documented
discriminant. Add it to the runtime helpers and the OpenAPI success/error
envelopes so responses match the reference. The existing data and error
fields are unchanged, keeping this backward compatible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: bb34ce3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
There was a problem hiding this comment.
The approach is correct: adding the documented success discriminant to the shared apiSuccess/apiError helpers and the OpenAPI envelope schemas is the right fix, and the new unit tests pin the helper output. However, the PR is incomplete. EmDash has many hand-rolled JSON responses that bypass these helpers, so after this change the API is inconsistent: the OpenAPI spec now declares success everywhere, but a meaningful slice of real responses (auth/CSRF errors, validation errors, scope errors, /_emdash/api/auth/mode, /_emdash/api/manifest, /_emdash/api/admin/plugins/updates, etc.) still emit bare { data } or { error }.
This is both a correctness issue (the published spec disagrees with runtime behavior for those endpoints) and an AGENTS.md convention violation ("Use the shared utilities -- don't roll your own" for API responses). I would either fix the remaining call sites before merge or narrow the PR claims/changeset; as it stands the changeset says "Fixes API responses to include the success field", which over-promises.
I checked the diff, the changed helpers/schemas, traced Response.json / new Response(JSON.stringify call sites across packages/core/src, and verified the client only reads data/error so existing consumers remain compatible. Tests compile against the old { data } casts but still pass because the new envelope is a superset.
Headline: mergeable once the hand-rolled responses are brought into line with the new envelope, or once the PR scope is honestly narrowed to "helpers and schemas only".
Findings
-
[needs fixing]
packages/core/src/api/parse.ts:116The validation failure path in
parseBody/parseQueryreturns a raw{ error: { code: "VALIDATION_ERROR", ... } }object without thesuccess: falsediscriminant. Because almost every route uses these parsers, this creates a large class of 400 responses that contradict the updated OpenAPIapiErrorSchema.return apiError( "VALIDATION_ERROR", "Invalid request data", 400, { issues }, );(The manual
Response.jsonand cache headers can then be removed.) -
[needs fixing]
packages/core/src/astro/middleware/auth.ts:63csrfRejectedResponse()hand-rolls an error body withoutsuccess: false. The same file also hand-rollsmcpUnauthorizedResponse()(line 77), the setup-route CSRF rejection (line 227), the invalid-bearer responses (lines 263 and 402), the passkeyNOT_AUTHENTICATED/NOT_FOUNDresponses (lines 665 and 683), and theINSUFFICIENT_SCOPEresponses inenforceTokenScope(lines 798 and 813). All of these are EmDash API error responses and should useapiError(...)so they match the documented{ success: false, error }envelope and include consistent cache headers. -
[needs fixing]
packages/core/src/auth/scopes.ts:24requireScope()returns a hand-rolled{ error: { code: "INSUFFICIENT_SCOPE", ... } }response without thesuccessdiscriminant. It duplicates the scope logic inauth.tsand should delegate toapiError("INSUFFICIENT_SCOPE", ...)for consistency with the OpenAPI error schema. -
[needs fixing]
packages/core/src/astro/routes/api/admin/plugins/updates.ts:64This route returns
Response.json({ data: { items } }), omittingsuccess: trueand the standard cache headers. The comment above it also becomes outdated after this PR. UseapiSuccess:return apiSuccess({ items }); -
[needs fixing]
packages/core/src/astro/routes/api/auth/mode.ts:43The public auth-mode endpoint returns a bare
{ data: { authMode, signupEnabled, providers } }envelope withoutsuccess: true. It should useapiSuccess(...)to align with the OpenAPI success envelope. -
[needs fixing]
packages/core/src/astro/routes/api/manifest.ts:97The admin manifest endpoint returns
Response.json({ data: manifest }, ...), missing thesuccess: truediscriminant. UseapiSuccess(manifest)and, if needed, set theX-Schema-Version-style header on the returned response. -
[needs fixing]
packages/core/src/astro/routes/api/admin/bylines/[id]/translations.ts:68The source-byline 404 is hand-rolled as
{ error: { code: "NOT_FOUND", ... } }withoutsuccess: false. Since neighbouring code usesapiErrorviaunwrapResultandhandleError, this should be:return apiError("NOT_FOUND", "Byline not found", 404); -
[suggestion]
packages/core/tests/integration/openapi/openapi.test.ts:122The "uses consistent error response shape across all error codes" test only asserts that an error response has a schema. Since the PR adds
success: falsetoapiErrorSchema, consider also asserting that every 4xx/5xx JSON schema has asuccessproperty (or is a$reftoApiError), mirroring the success-envelope check you added above.
Adding the success discriminant to the response envelope meant several
hand-rolled JSON responses no longer matched the documented shape: request
validation, the auth-middleware CSRF/scope/bearer rejections, and the
auth-mode, manifest, plugin-updates, and byline-translations routes still
emitted bare { data } / { error }. Route them through apiError/apiSuccess so
every response carries the success discriminant and consistent cache headers,
per the shared-utility convention. No auth logic changes -- response shape only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCP unauthorized and invalid-bearer-token responses carry a
WWW-Authenticate header for OAuth discovery, so they could not simply be
swapped for apiError without losing it. Build the response via apiError to
get the { success: false, error } envelope, then set the WWW-Authenticate
header on it, preserving the OAuth discovery handshake. Standards-based
OAuth token/registration and well-known responses are intentionally left
untouched -- they follow their own RFC formats, not the EmDash envelope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The initial PR only added
Behavior is unchanged apart from the added |
There was a problem hiding this comment.
Re-review of PR #2098. The author addressed all of the hand-rolled JSON response sites from the previous review: parseBody/parseQuery validation failures, auth middleware CSRF/token/scope errors, requireScope, /_emdash/api/admin/plugins/updates, /_emdash/api/auth/mode, /_emdash/api/manifest, and the byline translations 404 now all go through apiError/apiSuccess. The updated OpenAPI tests also assert the success discriminant on both success and error schemas.
The approach is still correct: adding the documented discriminant to the shared helpers and schemas, plus converting the bypass sites, makes the runtime envelope consistent with the REST API reference and generated spec while remaining backward compatible (existing data/error consumers are unaffected).
I traced remaining Response.json / JSON.stringify calls in packages/core/src and only found OAuth/well-known protocol endpoints, which legitimately use their RFC-mandated shapes rather than the EmDash envelope, and a few plain-text external-auth middleware responses. No other hand-rolled EmDash API envelopes remain.
Two small documentation/comment inconsistencies are still worth cleaning up before merge:
- The generated OpenAPI spec's own
info.descriptionstill says responses are "wrapped in a{ data }envelope", but the spec now declares{ success, data }. The description should match the schema it documents. - A comment in
visual-editing/toolbar.tsstill refers to a{ data }envelope (the code already handles the new shape correctly).
Headline: ready to merge once the generated spec description is updated.
Findings
-
[needs fixing]
packages/core/src/api/openapi/document.ts:2354The generated OpenAPI spec now declares the
{ success, data }success envelope and{ success, false, error }error schema, but its owninfo.descriptionstill tells readers responses are "wrapped in a{ data }envelope". This contradicts the schema and the PR's goal of making the spec agree with the docs.description: "REST API for the EmDash CMS. All endpoints require authentication and return responses wrapped in a `{ success, data }` envelope.", -
[suggestion]
packages/core/src/visual-editing/toolbar.ts:733The comment still describes the manifest response as a
{ data }envelope. The code below it (manifestCache = m && m.data ? m.data : m) already handles the current{ success, data }shape, so the comment is stale.// The manifest endpoint wraps the payload in a { success, data } envelope (ApiResponse shape).
What does this PR do?
The REST API reference documents a discriminated response envelope
{ success: true, data }on success and{ success: false, error }on error (see Response format and Plugin API routes). In practice thesuccessfield was never emitted:apiSuccess/apiErrorreturned bare{ data }/{ error }, and the generated OpenAPI schema (successEnvelope/apiErrorSchema) declared the same shape. Clients couldn't rely on the documented discriminant, and the published OpenAPI spec disagreed with the docs.This adds
successto both the runtime response helpers and the OpenAPI envelope schemas so responses match the reference. The existingdataanderrorfields are unchanged, so this is backward compatible — existing consumers that readdata/errorkeep working.Notably, the internal
ApiResult<T>type already carries thesuccessdiscriminant; it was only being dropped at the HTTP boundary.Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure