Skip to content

fix(web,api): contrato estable por code para localización de errores 4xx#421

Open
nopestack wants to merge 5 commits into
GlobalEmergency:mainfrom
nopestack:fix/348-error-localization-contract
Open

fix(web,api): contrato estable por code para localización de errores 4xx#421
nopestack wants to merge 5 commits into
GlobalEmergency:mainfrom
nopestack:fix/348-error-localization-contract

Conversation

@nopestack

Copy link
Copy Markdown
Contributor

Closes #348

Contexto

Notas de la review de #338 (mergeado, cerró #296): localizeBackendError
(apps/web/src/lib/backend-error-messages.ts) mapea errores del backend a
copy localizado matcheando por substring/prefijo del .message en inglés de
las excepciones de dominio. Funciona hoy, pero el contrato es texto libre sin
nada que enlace apps/api y apps/web: si alguien reescribe un .message
en el backend, la web degrada en silencio al mensaje genérico, y los tests
existentes hardcodeaban las mismas cadenas por lo que no detectaban el
drift.

Qué opción se implementó

Se implementó el fix real (opción preferida del issue), no el mínimo
intermedio: expongo un code estable en las excepciones de dominio y lo
devuelvo en el body de los filtros de excepción NestJS.

Descarté el mínimo intermedio (test que importa las clases reales y afirma
que su .message sigue casando cada patrón) porque, tras localizar las 12
clases detrás de los patrones de KNOWN_BACKEND_ERRORS, el alcance real
resultó acotado: son sólo 9 clases de error, concentradas en 5 contextos
(needs, offers, logistics, resources, supplies) más
packages/warehouse-core — no hizo falta tocar identity, groups,
organizations, volunteers, accreditation, templates,
notifications ni el resto de los ~150 errores de dominio del repo. Con
ese alcance, el fix real era viable sin convertirse en un PR gigante.

Cambios

  • packages/warehouse-core: SupplyLineValidationError,
    CapacityMustHaveWeightOrVolumeError, InvalidCapacityAmountError,
    InvalidCoverageError, InvalidCapacityWindowError ahora exponen
    .code. Para las clases con más de un mensaje posible
    (SupplyLineValidationError, InvalidCapacityWindowError), el code se
    pasa en el punto de throw; InvalidCoverageError lo deja opcional
    porque sólo uno de sus 5 sitios de throw tiene copy en la web hoy — el
    resto sigue degradando al fallback genérico sin romper nada.
  • apps/api: NeedResourceNotInEmergencyError,
    TargetNeedNotFoundError, TargetNeedWrongEmergencyError,
    OfferItemsRequiredError exponen .code. Nuevo helper compartido
    apps/api/src/shared/http/domain-error-response.ts
    (domainErrorResponseBody) usado por los 5 filtros de excepción
    afectados (needs, offers, logistics, resources, supplies):
    añade code al body sólo cuando la excepción lo expone, así que sigue
    siendo backward compatible con { statusCode, message }.
  • apps/web: localizeBackendError ahora resuelve primero por code
    (match directo t[code]) y sólo cae al matching por prosa (la tabla
    KNOWN_BACKEND_ERRORS) como red de seguridad transicional — útil durante
    la ventana de deploy-lag entre web y API (despliegan por separado) y para
    cualquier error que aún no haya migrado a code. La firma cambió de
    recibir el .message ya extraído a recibir el error completo del
    cliente tipado, lo que además simplificó los 5 call-sites
    (peticion, donar/ofrecer, registrar, voluntario,
    ofrecer-transporte) quitándoles la extracción manual de rawMessage.
  • Se añadió la cobertura de test que faltaba para offer_items_required
    en backend-error-messages.test.ts, más tests nuevos para el camino por
    code, el fallback por prosa, y el code de cada excepción de dominio
    tocada (en apps/api y packages/warehouse-core).

No se tocó packages/api-client/src/schema.ts: no hay decoradores
@ApiResponse documentando el shape de error en estos endpoints, así que
pnpm gen:api no produce diff (verificado).

Nota UX opcional del issue — diferida

El punto opcional de limpiar el resaltado de fila inválida
(InventoryField/SupplyLineList) en el primer onChange tras un error
no se incluyó: el estado invalidRow se maneja de forma independiente en
~6 formularios distintos (peticion, registrar, pre-registro,
create-shipment, inventory-edit-form, record-inventory-entry), así
que no es un cambio trivial de un solo punto. Queda fuera de este PR.

Gate local

  • pnpm install --frozen-lockfile
  • pnpm --filter '@globalemergency/warehouse-core' build && test (213 tests)
  • pnpm --filter '@globalemergency/warehouse-postgres' build
  • pnpm --filter api build
  • pnpm --filter api exec eslint "{src,apps,libs,test}/**/*.ts" --max-warnings=0
  • pnpm --filter api exec prettier --check "src/**/*.ts" "test/**/*.ts"
  • pnpm --filter api test (259 suites / 1654 tests)
  • pnpm --filter @reliefhub/api-client build && pnpm --filter web build
  • pnpm --filter web lint
  • pnpm --filter web test (122 tests)
  • pnpm gen:api → sin diff en schema.ts/openapi.json

Todo en verde.

nopestack added 5 commits July 6, 2026 09:54
…es 4xx

Seguimiento de GlobalEmergency#338 (GlobalEmergency#348): la web matcheaba errores de dominio por
substring/prefijo del `.message` en inglés, un acoplamiento frágil que
degrada en silencio si el backend cambia la prosa y que los tests de
`backend-error-messages.test.ts` no detectaban (hardcodeaban las mismas
cadenas).

Se añade un `code` estable a las excepciones de dominio detrás de los 12
patrones de KNOWN_BACKEND_ERRORS (SupplyLineValidationError,
NeedResourceNotInEmergencyError, TargetNeedNotFoundError/
TargetNeedWrongEmergencyError, OfferItemsRequiredError,
CapacityMustHaveWeightOrVolumeError, InvalidCapacityAmountError,
InvalidCoverageError, InvalidCapacityWindowError) y se propaga en el body
JSON de los filtros de excepción de needs/offers/logistics/resources/
supplies vía un helper compartido (`domainErrorResponseBody`). La web
(`localizeBackendError`) ahora resuelve primero por `code` y sólo cae al
matching por prosa como red de seguridad transicional (deploy-lag web/API)
o para errores aún no migrados.

Se añade también la cobertura de test que faltaba para
`offer_items_required` en backend-error-messages.test.ts.

La nota de UX opcional (limpiar el resaltado de fila inválida en el primer
onChange tras un error) se deja fuera: toca el estado independiente de
~6 formularios distintos, no es un cambio trivial.
@nopestack
nopestack requested a review from vgpastor as a code owner July 17, 2026 08:53
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

@nopestack is attempting to deploy a commit to the GlobalEmergency Team on Vercel.

A member of the Team first needs to authorize it.

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.

[TASK] Seguimiento de #338: contrato estable para la localización de errores 4xx

1 participant