From fa8339003d2f208fff7526db6daf8436af8097f3 Mon Sep 17 00:00:00 2001 From: B2BCall Bootstrap Date: Thu, 27 Aug 2026 20:48:00 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20bug=20sist=C3=AAmico=20no=20cliente=20HT?= =?UTF-8?q?TP=20quebrava=20toda=20a=C3=A7=C3=A3o=20sem=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Causa raiz do 'body is empty' relatado pelo usuário ao clicar em redefinir senha do ramal: o cliente sempre mandava Content-Type: application/json mesmo em requisições sem corpo (POST/PATCH de ação, ex.: reset-password, agent-console available/pause/unpause/logout, dialplan publish/rollback, campanha start/pause/stop/drain). O Fastify rejeita isso com 400 'Body cannot be empty when content-type is set to application/json' — nunca detectado antes porque todo teste anterior foi via curl sem fixar esse header, não reproduzindo o comportamento real do fetch() do navegador. Corrigido na única função request() central (apps/frontend/src/lib/api-client.ts): só envia o header quando há de fato um corpo. Bug relacionado encontrado na mesma revisão: DELETE /api/suppression/:id exige removalReason no corpo, mas api.delete() nem aceitava um argumento de corpo — o botão 'Desbloquear' da lista de bloqueio sempre falhava com 403. api.delete() agora aceita body opcional; a tela pede o motivo via prompt antes de remover. --- apps/frontend/src/app/(app)/bloqueio/page.tsx | 10 ++++++++-- apps/frontend/src/lib/api-client.ts | 8 ++++++-- apps/frontend/src/services/suppression.ts | 3 ++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/app/(app)/bloqueio/page.tsx b/apps/frontend/src/app/(app)/bloqueio/page.tsx index 393b758..9dfd8f9 100644 --- a/apps/frontend/src/app/(app)/bloqueio/page.tsx +++ b/apps/frontend/src/app/(app)/bloqueio/page.tsx @@ -95,7 +95,8 @@ function SuppressionContent() { }); const remove = useMutation({ - mutationFn: (id: string) => suppressionService.remove(id), + mutationFn: ({ id, removalReason }: { id: string; removalReason: string }) => + suppressionService.remove(id, removalReason), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['suppression'] }); toast({ title: 'Número desbloqueado', variant: 'success' }); @@ -131,7 +132,12 @@ function SuppressionContent() { variant="ghost" className="text-destructive" onClick={() => { - if (confirm(`Desbloquear ${r.phoneNormalized}?`)) remove.mutate(r.id); + const removalReason = window.prompt( + `Motivo para desbloquear ${r.phoneNormalized} (obrigatório):`, + ); + if (removalReason && removalReason.trim().length > 0) { + remove.mutate({ id: r.id, removalReason: removalReason.trim() }); + } }} > diff --git a/apps/frontend/src/lib/api-client.ts b/apps/frontend/src/lib/api-client.ts index 50c7b12..f8062ad 100644 --- a/apps/frontend/src/lib/api-client.ts +++ b/apps/frontend/src/lib/api-client.ts @@ -61,7 +61,11 @@ async function request( const init: RequestInit = { method, credentials: 'include', - headers: isForm ? undefined : { 'Content-Type': 'application/json' }, + // Content-Type: application/json sem corpo faz o Fastify rejeitar a + // requisição ("Body cannot be empty when content-type is set to + // 'application/json'") — só envia o header quando há de fato um corpo + // (ex.: POST de ação sem payload, como reset-password/publish/start). + headers: isForm || body === undefined ? undefined : { 'Content-Type': 'application/json' }, body: isForm ? (body as FormData) : body !== undefined @@ -119,7 +123,7 @@ export const api = { request('GET', path, { params }), post: (path: string, body?: unknown) => request('POST', path, { body }), patch: (path: string, body?: unknown) => request('PATCH', path, { body }), - delete: (path: string) => request('DELETE', path), + delete: (path: string, body?: unknown) => request('DELETE', path, { body }), upload: (path: string, form: FormData, params?: object) => request('POST', path, { body: form, isForm: true, params }), download: (path: string, params?: object) => diff --git a/apps/frontend/src/services/suppression.ts b/apps/frontend/src/services/suppression.ts index b5fb921..7915cb2 100644 --- a/apps/frontend/src/services/suppression.ts +++ b/apps/frontend/src/services/suppression.ts @@ -17,5 +17,6 @@ export const suppressionService = { form.append('file', file); return api.upload('/suppression/import', form); }, - remove: (id: string) => api.delete(`/suppression/${id}`), + remove: (id: string, removalReason: string) => + api.delete(`/suppression/${id}`, { removalReason }), };