Pular para o conteúdo

Cliente HTTP

Toda integração precisa das mesmas quatro garantias: a chave fica no servidor, cada escrita leva uma Idempotency-Key, recusas temporárias esperam Retry-After e nenhum erro vira sucesso. Este cliente resolve as quatro em menos de 80 linhas, sem SDK. Copie, ajuste o registro de logs e use nas suas chamadas.

cotejar.mjs
// Cliente mínimo da API Cotejar para Node.js 20+, sem dependências.
const BASE = process.env.COTEJAR_API_BASE_URL ?? 'https://api.cotejar.com.br'
const CHAVE = process.env.COTEJAR_API_KEY
if (!CHAVE) throw new Error('Defina COTEJAR_API_KEY no ambiente do servidor.')
export class ErroCotejar extends Error {
constructor(status, corpo, requestId) {
super(`HTTP ${status} ${corpo?.erro?.codigo ?? ''} (request_id ${requestId ?? 'ausente'})`)
this.status = status
this.codigo = corpo?.erro?.codigo
this.requestId = requestId
}
}
const esperar = ms => new Promise(resolver => setTimeout(resolver, ms))
/**
* Faz uma chamada e devolve { status, dados, etag, location }.
* Repete só o que é seguro repetir: leituras, e escritas com a MESMA chave de
* idempotência. Erro de negócio (4xx) nunca é repetido.
*/
export async function chamar(metodo, caminho, { corpo, idempotencia, ifMatch, tentativas = 4 } = {}) {
if (metodo !== 'GET' && !idempotencia) throw new Error(`${metodo} ${caminho} exige chave de idempotência.`)
for (let tentativa = 1; ; tentativa++) {
let resposta
try {
resposta = await fetch(new URL(caminho, BASE), {
method: metodo,
headers: {
authorization: `Bearer ${CHAVE}`,
...(corpo !== undefined && { 'content-type': 'application/json' }),
...(idempotencia && { 'idempotency-key': idempotencia }),
...(ifMatch && { 'if-match': ifMatch }),
},
body: corpo === undefined ? undefined : JSON.stringify(corpo),
redirect: 'error',
signal: AbortSignal.timeout(30_000),
})
} catch (erro) {
// Rede ou timeout: o pedido pode ter chegado. Repetir com a mesma chave é seguro.
if (tentativa < tentativas) { await esperar(500 * 2 ** tentativa); continue }
throw erro
}
if ((resposta.status === 429 || resposta.status === 503) && tentativa < tentativas) {
const segundos = Number(resposta.headers.get('retry-after'))
await resposta.body?.cancel()
await esperar(Number.isFinite(segundos) && segundos > 0 ? segundos * 1000 : 1000 * 2 ** tentativa)
continue
}
const texto = await resposta.text()
const dados = texto ? JSON.parse(texto) : null
if (!resposta.ok) throw new ErroCotejar(resposta.status, dados, resposta.headers.get('x-request-id'))
return { status: resposta.status, dados, etag: resposta.headers.get('etag'), location: resposta.headers.get('location') }
}
}
/** Consulta o recurso até um estado final. Timeout nunca vira sucesso. */
export async function aguardar(caminho, estadosFinais, { intervaloMs = 2_000, prazoMs = 15 * 60_000 } = {}) {
const limite = Date.now() + prazoMs
for (;;) {
const { dados } = await chamar('GET', caminho)
if (estadosFinais.includes(dados.estado)) return dados
if (Date.now() > limite) throw new Error(`Prazo esgotado em ${caminho}; último estado: ${dados.estado}`)
await esperar(intervaloMs)
}
}
import { randomUUID } from 'node:crypto'
import { aguardar, chamar, ErroCotejar } from './cotejar.mjs'
const { dados: conta } = await chamar('GET', '/v1/conta')
// Escrita: gere a chave uma vez por operação lógica e guarde junto do pedido.
const chave = randomUUID()
const pedido = await chamar('POST', '/v1/analises', { corpo: {}, idempotencia: chave })
const analise = await aguardar(pedido.location, ['concluida', 'falhou'])
try {
await chamar('PATCH', `/v1/catalogos/${id}`, { corpo: { nome }, idempotencia: randomUUID(), ifMatch: etag })
} catch (erro) {
if (erro instanceof ErroCotejar && erro.status === 412) {
// O catálogo mudou: leia de novo e decida se a alteração ainda vale.
} else throw erro
}
Situação Comportamento
Escrita sem Idempotency-Key Recusa antes de enviar. Toda mutação da API exige a chave.
Falha de rede ou timeout Repete com a mesma chave. A API devolve o recibo original se o primeiro envio tiver chegado.
429 ou 503 Espera Retry-After (ou recuo exponencial) e repete, até quatro tentativas.
Qualquer outro 4xx ou 5xx Lança ErroCotejar com status, codigo e request_id. Nunca repete.
Redirecionamento Recusa. A chave não deve seguir para outro endereço.
Estado assíncrono aguardar consulta até um estado final e falha ao fim do prazo, em vez de supor sucesso.

Os dois arquivos foram executados contra uma instância local da API: leitura, criação de ingestão, repetição idempotente (repetido: true) e conflito (409 LOTE_CONFLITANTE com request_id).