Skip to content

feat(agent-bff): map agent errors to the BFF error contract#1738

Open
nbouliol wants to merge 2 commits into
mainfrom
feature/prd-670-map-agent-errors-to-the-bff-error-contract
Open

feat(agent-bff): map agent errors to the BFF error contract#1738
nbouliol wants to merge 2 commits into
mainfrom
feature/prd-670-map-agent-errors-to-the-bff-error-contract

Conversation

@nbouliol

@nbouliol nbouliol commented Jul 6, 2026

Copy link
Copy Markdown
Member

Context

Slice-3 BFF data/action endpoints (PRD-671..674) need one shared error layer producing the BFF envelope { error: { type, status, message, details? } }, so consumers branch on error.type + error.status, never on a message substring. This ships first and is a hard dependency of every Slice-3 endpoint ticket.

What this adds

  • src/http/agent-error-mapper.tsmapAgentError(error, { logger }): BffHttpError:
    • transport failure (raw error, not an AgentHttpError) → network_error (502)
    • agent 5xx → agent_unavailable (503, normalized)
    • JSON:API errors[0] → type from an explicit name registry (NotFoundErrornot_found, …), status from errors[0].status, message from detail, details from data
    • unknown agent name → status-based fallback type + logger warn (surfaces the gap without collapsing to a generic type)
    • defensive parse of an Error whose message is a JSON { errors: [...] } string
  • src/http/bff-local-errors.ts — the complete registry of BFF-local factories the Slice-3 endpoints emit: unknown_* (404), *_not_allowed (403), invalid_request (400), relation_field_not_supported (422), mapping_error (500), unsupported_action_result (501).
  • package.json — adds @forestadmin/agent-client (the committed BFF→agent transport; AgentHttpError is the mapper input).

Produced BffHttpError instances flow through the existing error-middleware unchanged. No barrel exports: in-package endpoints import relatively.

Notes / deviations from the ticket text

The ticket and the global spec were stale on two points; the implementation follows the actual code:

  • agent-client throws a typed AgentHttpError (not Error(JSON.stringify(...))); the string case is kept as a defensive fallback.
  • Agent JSON:API errors carry data, not meta/source; details is sourced from data.
  • Agent error names are suffixed (NotFoundError, not NotFound), so an explicit registry replaces literal snake_case.

network_error=502 / agent_unavailable=503 are chosen defaults (unspecified by the spec).

Tests

  • test/http/agent-error-mapper.test.ts — 20 cases (registry coverage, JSON-in-message, transport→502, 5xx→503, flat body→400, unmapped-name warn).
  • test/http/bff-local-errors.test.ts — 11 cases (every factory + details).

Full suite: 389/389 pass; build + lint clean.

Fixes PRD-670

🤖 Generated with Claude Code

Note

Map agent errors to the BFF error contract in agent-bff

  • Adds agent-error-mapper.ts with a mapAgentError function that converts AgentHttpError, plain Error, JSON:API bodies, and flat bodies into typed BffHttpError instances.
  • JSON:API errors are matched by name via AGENT_ERROR_TYPE_MAP; unmapped names produce a mapping_error (500) and a log entry; server errors (status ≥ 500) normalize to agent_unavailable (503); non-AgentHttpError instances become network_error (502).
  • Adds bff-local-errors.ts with factory functions for all local BFF error types (404 unknown_collection/unknown_relation/unknown_action, 403 collection_not_allowed etc., 400 invalid_request, 422 relation_field_not_supported, 500 mapping_error, 501 unsupported_action_result).
  • Risk: unmapped agent error names silently downgrade to mapping_error (500) rather than propagating the original status code.

Macroscope summarized 886cec1.

Add mapAgentError() translating agent-client AgentHttpError failures into
the BFF error envelope, plus the complete registry of BFF-local error
factories the Slice-3 endpoints emit.

Fixes PRD-670

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 6, 2026

Copy link
Copy Markdown

PRD-670

@qltysh

qltysh Bot commented Jul 6, 2026

Copy link
Copy Markdown

1 new issue

Tool Category Rule Count
qlty Structure Function with many returns (count = 5): mapAgentError 1


function mapJsonApiError(agentError: AgentJsonApiError, logger: Logger): BffHttpError {
const status = agentError.status ?? STATUS_BAD_REQUEST;
const message = agentError.detail ?? DEFAULT_ERROR_MESSAGE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium http/agent-error-mapper.ts:74

mapJsonApiError reads only agentError.detail for the outgoing message, so when an agent JSON:API error provides message without detail, it returns the generic "Unexpected error" instead of the server-supplied message. Consider falling back to agentError.message before DEFAULT_ERROR_MESSAGE.

Suggested change
const message = agentError.detail ?? DEFAULT_ERROR_MESSAGE;
const message = agentError.detail ?? agentError.message ?? DEFAULT_ERROR_MESSAGE;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/http/agent-error-mapper.ts around line 74:

`mapJsonApiError` reads only `agentError.detail` for the outgoing message, so when an agent JSON:API error provides `message` without `detail`, it returns the generic `"Unexpected error"` instead of the server-supplied message. Consider falling back to `agentError.message` before `DEFAULT_ERROR_MESSAGE`.

Comment on lines +72 to +74
function mapJsonApiError(agentError: AgentJsonApiError, logger: Logger): BffHttpError {
const status = agentError.status ?? STATUS_BAD_REQUEST;
const message = agentError.detail ?? DEFAULT_ERROR_MESSAGE;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High http/agent-error-mapper.ts:72

mapJsonApiError reads status from the JSON:API error object and falls back to 400 when it is missing. When an AgentHttpError carries the status only on the outer error (e.g. new AgentHttpError(403, { errors: [{ name: 'ForbiddenError' }] }, 'Forbidden')), the resulting BffHttpError gets status 400 instead of 403, so the BFF returns the wrong HTTP status to clients. Pass the outer AgentHttpError.status into mapJsonApiError and use it as the fallback instead of 400.

-function mapJsonApiError(agentError: AgentJsonApiError, logger: Logger): BffHttpError {
-  const status = agentError.status ?? STATUS_BAD_REQUEST;
+function mapJsonApiError(
+  agentError: AgentJsonApiError,
+  fallbackStatus: number,
+  logger: Logger,
+): BffHttpError {
+  const status = agentError.status ?? fallbackStatus;
   const message = agentError.detail ?? DEFAULT_ERROR_MESSAGE;
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/http/agent-error-mapper.ts around lines 72-74:

`mapJsonApiError` reads `status` from the JSON:API error object and falls back to `400` when it is missing. When an `AgentHttpError` carries the status only on the outer error (e.g. `new AgentHttpError(403, { errors: [{ name: 'ForbiddenError' }] }, 'Forbidden')`), the resulting `BffHttpError` gets status `400` instead of `403`, so the BFF returns the wrong HTTP status to clients. Pass the outer `AgentHttpError.status` into `mapJsonApiError` and use it as the fallback instead of `400`.

@qltysh

qltysh Bot commented Jul 6, 2026

Copy link
Copy Markdown

Qlty


Coverage Impact

⬆️ Merging this pull request will increase total coverage on main by 0.01%.

Modified Files with Diff Coverage (2)

RatingFile% DiffUncovered Line #s
New Coverage rating: A
packages/agent-bff/src/http/agent-error-mapper.ts98.2%86
New Coverage rating: A
packages/agent-bff/src/http/bff-local-errors.ts100.0%
Total98.7%
🤖 Increase coverage with AI coding...
In the `feature/prd-670-map-agent-errors-to-the-bff-error-contract` branch, add test coverage for this new code:

- `packages/agent-bff/src/http/agent-error-mapper.ts` -- Line 86

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

- unmapped agent error name now returns 500 mapping_error instead of a
  silent status fallback
- extract the unmapped-name warn out of fallbackTypeByStatus (single
  responsibility)
- use a named constant for the validation_error type
- make the name-to-type test use an explicit table instead of deriving
  expectations from the map under test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
if (agentError.name !== undefined) {
const type = AGENT_ERROR_TYPE_MAP[agentError.name];

if (type === undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High http/agent-error-mapper.ts:77

When agentError.name is present but not in AGENT_ERROR_TYPE_MAP, mapJsonApiError returns mappingError(...), which always produces a BffHttpError with status 500 and type mapping_error. This overwrites the agent's original status (e.g. a 4xx) and reports a client error to BFF consumers as an internal server-side mapping failure. Per the PR contract, unknown names should log and fall back to the status-based type using fallbackTypeByStatus(status).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent-bff/src/http/agent-error-mapper.ts around line 77:

When `agentError.name` is present but not in `AGENT_ERROR_TYPE_MAP`, `mapJsonApiError` returns `mappingError(...)`, which always produces a `BffHttpError` with status `500` and type `mapping_error`. This overwrites the agent's original status (e.g. a 4xx) and reports a client error to BFF consumers as an internal server-side mapping failure. Per the PR contract, unknown names should log and fall back to the status-based type using `fallbackTypeByStatus(status)`.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant