Documentation Index Fetch the complete documentation index at: https://docs.connectvets.com.br/llms.txt
Use this file to discover all available pages before exploring further.
Visão Geral
O ConnectVets Notes utiliza códigos de status HTTP padrão e retorna respostas estruturadas para todos os tipos de erro. Uma estratégia robusta de tratamento de erros é essencial para uma integração confiável.
Boa prática : Sempre implemente retry automático com backoff exponencial para erros temporários.
Códigos de Status HTTP
Códigos de Sucesso (2xx)
Código Significado Exemplo 200OK - Requisição bem-sucedidaGET /notes retorna lista201Created - Recurso criado com sucessoPOST /notes cria nova nota204No Content - Operação bem-sucedida, sem conteúdoDELETE /notes/{id}
Códigos de Erro do Cliente (4xx)
Erros de Autenticação
401 Unauthorized - API Key inválida
403 Forbidden - Sem permissão para a operação
429 Too Many Requests - Rate limit atingido
Erros de Dados
400 Bad Request - Dados inválidos
404 Not Found - Recurso não encontrado
413 Payload Too Large - Arquivo muito grande
422 Unprocessable Entity - Dados mal formados
Códigos de Erro do Servidor (5xx)
Código Significado Ação Recomendada 500Internal Server Error Retry com backoff 502Bad Gateway Retry após alguns segundos 503Service Unavailable Retry após delay maior 504Gateway Timeout Retry com timeout maior
Estrutura de Resposta de Erro
{
"error" : {
"code" : "validation_error" ,
"message" : "O arquivo de áudio é obrigatório" ,
"details" : {
"field" : "audio" ,
"reason" : "missing_required_field"
},
"request_id" : "req_1234567890abcdef" ,
"timestamp" : "2024-02-14T18:25:43Z"
}
}
Códigos de Erro Específicos
invalid_api_key{
"error" : {
"code" : "invalid_api_key" ,
"message" : "API key inválida ou expirada" ,
"details" : {
"provided_key" : "cvn_live_abc123..." ,
"hint" : "Verifique se a chave está correta e ativa"
}
}
}
insufficient_permissions{
"error" : {
"code" : "insufficient_permissions" ,
"message" : "Chave não tem permissão para esta operação" ,
"details" : {
"required_permission" : "write" ,
"current_permission" : "read"
}
}
}
file_too_large{
"error" : {
"code" : "file_too_large" ,
"message" : "Arquivo excede o tamanho máximo permitido" ,
"details" : {
"file_size" : 104857600 ,
"max_size" : 104857600 ,
"max_size_mb" : 100
}
}
}
unsupported_format{
"error" : {
"code" : "unsupported_format" ,
"message" : "Formato de arquivo não suportado" ,
"details" : {
"provided_format" : "txt" ,
"supported_formats" : [ "mp3" , "wav" , "m4a" , "aac" ]
}
}
}
🎵 Erros de Processamento de Áudio
audio_quality_poor{
"error" : {
"code" : "audio_quality_poor" ,
"message" : "Qualidade do áudio insuficiente para processamento" ,
"details" : {
"issues" : [ "volume_too_low" , "excessive_noise" ],
"recommendations" : [
"Aumentar volume do microfone" ,
"Reduzir ruído de fundo"
]
}
}
}
audio_duration_invalid{
"error" : {
"code" : "audio_duration_invalid" ,
"message" : "Duração do áudio fora dos limites permitidos" ,
"details" : {
"duration" : 5 ,
"min_duration" : 10 ,
"max_duration" : 3600
}
}
}
quota_exceeded{
"error" : {
"code" : "quota_exceeded" ,
"message" : "Cota mensal de processamento excedida" ,
"details" : {
"current_usage" : 1500 ,
"quota_limit" : 1000 ,
"reset_date" : "2024-03-01T00:00:00Z"
}
}
}
subscription_expired{
"error" : {
"code" : "subscription_expired" ,
"message" : "Assinatura expirada" ,
"details" : {
"expired_at" : "2024-02-01T00:00:00Z" ,
"renewal_url" : "https://notes.connectvets.com.br/billing"
}
}
}
Implementação de Tratamento de Erros
class ConnectVetsClient {
constructor ( apiKey , options = {}) {
this . apiKey = apiKey ;
this . baseUrl = options . baseUrl || 'https://api.connectvets.com' ;
this . maxRetries = options . maxRetries || 3 ;
this . retryDelay = options . retryDelay || 1000 ;
}
async request ( endpoint , options = {}) {
const url = ` ${ this . baseUrl }${ endpoint } ` ;
const config = {
... options ,
headers: {
'X-API-KEY' : this . apiKey ,
... options . headers
}
};
return this . executeWithRetry (() => fetch ( url , config ));
}
async executeWithRetry ( operation , attempt = 1 ) {
try {
const response = await operation ();
// Verificar se a resposta é bem-sucedida
if ( response . ok ) {
return response . json ();
}
// Tratar erro baseado no status
const errorData = await response . json (). catch (() => ({}));
const error = new ConnectVetsError ( response . status , errorData , response );
// Verificar se deve tentar novamente
if ( this . shouldRetry ( error , attempt )) {
return this . retryRequest ( operation , attempt );
}
throw error ;
} catch ( error ) {
// Erros de rede ou outros
if ( error instanceof ConnectVetsError ) {
throw error ;
}
// Retry para erros de rede
if ( attempt < this . maxRetries ) {
return this . retryRequest ( operation , attempt );
}
throw new ConnectVetsError ( 0 , {
code: 'network_error' ,
message: error . message
});
}
}
shouldRetry ( error , attempt ) {
// Não retry se já atingiu o máximo
if ( attempt >= this . maxRetries ) {
return false ;
}
// Retry para erros de servidor (5xx)
if ( error . status >= 500 ) {
return true ;
}
// Retry para rate limit
if ( error . status === 429 ) {
return true ;
}
// Retry para erros específicos
const retryableCodes = [
'service_unavailable' ,
'gateway_timeout' ,
'processing_timeout'
];
return retryableCodes . includes ( error . code );
}
async retryRequest ( operation , attempt ) {
const delay = this . calculateDelay ( attempt );
console . log ( `⏳ Tentativa ${ attempt + 1 } em ${ delay } ms...` );
await new Promise ( resolve => setTimeout ( resolve , delay ));
return this . executeWithRetry ( operation , attempt + 1 );
}
calculateDelay ( attempt ) {
// Backoff exponencial com jitter
const baseDelay = this . retryDelay ;
const exponentialDelay = baseDelay * Math . pow ( 2 , attempt - 1 );
const jitter = Math . random () * 0.1 * exponentialDelay ;
return Math . min ( exponentialDelay + jitter , 30000 ); // Max 30s
}
}
// Classe de erro customizada
class ConnectVetsError extends Error {
constructor ( status , errorData , response = null ) {
const message = errorData . error ?. message || errorData . message || 'Erro desconhecido' ;
super ( message );
this . name = 'ConnectVetsError' ;
this . status = status ;
this . code = errorData . error ?. code || errorData . code || 'unknown_error' ;
this . details = errorData . error ?. details || errorData . details || {};
this . requestId = errorData . error ?. request_id || errorData . request_id ;
this . response = response ;
}
isRetryable () {
return this . status >= 500 || this . status === 429 ;
}
isAuthError () {
return this . status === 401 || this . status === 403 ;
}
isValidationError () {
return this . status === 400 || this . status === 422 ;
}
isQuotaError () {
return this . code === 'quota_exceeded' || this . code === 'subscription_expired' ;
}
}
const client = new ConnectVetsClient ( process . env . CONNECTVETS_API_KEY , {
maxRetries: 3 ,
retryDelay: 2000
});
async function createNoteWithErrorHandling ( audioFile , metadata ) {
try {
console . log ( '📤 Enviando áudio para processamento...' );
const formData = new FormData ();
formData . append ( 'audio' , audioFile );
formData . append ( 'metadata' , JSON . stringify ( metadata ));
const note = await client . request ( '/notes' , {
method: 'POST' ,
body: formData
});
console . log ( '✅ Nota criada com sucesso:' , note . id );
return note ;
} catch ( error ) {
console . error ( '❌ Erro ao criar nota:' , error . message );
// Tratar diferentes tipos de erro
if ( error . isAuthError ()) {
handleAuthError ( error );
} else if ( error . isValidationError ()) {
handleValidationError ( error );
} else if ( error . isQuotaError ()) {
handleQuotaError ( error );
} else if ( error . isRetryable ()) {
handleRetryableError ( error );
} else {
handleUnknownError ( error );
}
throw error ;
}
}
function handleAuthError ( error ) {
console . error ( '🔐 Erro de autenticação:' , error . message );
if ( error . code === 'invalid_api_key' ) {
// Notificar sobre API key inválida
notifyUser ( 'API Key inválida. Verifique suas configurações.' );
} else if ( error . code === 'insufficient_permissions' ) {
// Notificar sobre permissões
notifyUser ( 'Sem permissão para esta operação. Upgrade necessário.' );
}
}
function handleValidationError ( error ) {
console . error ( '📝 Erro de validação:' , error . message );
const { details } = error ;
if ( details . field === 'audio' ) {
notifyUser ( 'Problema com o arquivo de áudio. Verifique o formato e tamanho.' );
} else if ( details . field === 'metadata' ) {
notifyUser ( 'Metadados inválidos. Verifique os campos obrigatórios.' );
}
// Mostrar detalhes específicos
console . log ( '📋 Detalhes:' , details );
}
function handleQuotaError ( error ) {
console . error ( '💰 Erro de cota:' , error . message );
if ( error . code === 'quota_exceeded' ) {
const { current_usage , quota_limit , reset_date } = error . details ;
notifyUser ( `Cota excedida ( ${ current_usage } / ${ quota_limit } ). Renova em ${ reset_date } ` );
} else if ( error . code === 'subscription_expired' ) {
const { renewal_url } = error . details ;
notifyUser ( 'Assinatura expirada. Renove sua assinatura.' , renewal_url );
}
}
function handleRetryableError ( error ) {
console . error ( '🔄 Erro temporário:' , error . message );
notifyUser ( 'Erro temporário. Tentando novamente...' );
}
function handleUnknownError ( error ) {
console . error ( '❓ Erro desconhecido:' , error );
// Reportar erro para monitoramento
reportError ( error );
notifyUser ( 'Erro interno. Nossa equipe foi notificada.' );
}
Estratégias de Recovery
class RetryHandler {
static async executeWithRetry (
operation ,
options = {}
) {
const {
maxRetries = 3 ,
initialDelay = 1000 ,
maxDelay = 30000 ,
backoffFactor = 2 ,
jitter = true
} = options ;
let lastError ;
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await operation ();
} catch ( error ) {
lastError = error ;
// Não retry para erros não retryable
if ( ! this . isRetryable ( error )) {
throw error ;
}
// Última tentativa
if ( attempt === maxRetries - 1 ) {
break ;
}
// Calcular delay
const delay = this . calculateDelay (
attempt ,
initialDelay ,
maxDelay ,
backoffFactor ,
jitter
);
console . log ( `⏳ Retry ${ attempt + 1 } / ${ maxRetries } em ${ delay } ms` );
await this . sleep ( delay );
}
}
throw lastError ;
}
static isRetryable ( error ) {
// Erros de rede
if ( error . code === 'ECONNRESET' || error . code === 'ETIMEDOUT' ) {
return true ;
}
// Erros HTTP retryable
if ( error . status >= 500 || error . status === 429 ) {
return true ;
}
// Códigos específicos retryable
const retryableCodes = [
'service_unavailable' ,
'gateway_timeout' ,
'processing_timeout' ,
'temporary_error'
];
return retryableCodes . includes ( error . code );
}
static calculateDelay ( attempt , initial , max , factor , jitter ) {
let delay = initial * Math . pow ( factor , attempt );
if ( jitter ) {
// Adicionar jitter de ±25%
const jitterAmount = delay * 0.25 ;
delay += ( Math . random () - 0.5 ) * jitterAmount * 2 ;
}
return Math . min ( Math . max ( delay , 0 ), max );
}
static sleep ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ));
}
}
// Uso do RetryHandler
async function uploadAudioWithRetry ( audioFile , metadata ) {
return RetryHandler . executeWithRetry (
() => client . createNote ( audioFile , metadata ),
{
maxRetries: 5 ,
initialDelay: 2000 ,
maxDelay: 60000 ,
backoffFactor: 2 ,
jitter: true
}
);
}
Circuit Breaker Pattern
class CircuitBreaker {
constructor ( options = {}) {
this . failureThreshold = options . failureThreshold || 5 ;
this . resetTimeout = options . resetTimeout || 60000 ;
this . monitoringPeriod = options . monitoringPeriod || 10000 ;
this . state = 'CLOSED' ; // CLOSED, OPEN, HALF_OPEN
this . failureCount = 0 ;
this . lastFailureTime = null ;
this . successCount = 0 ;
}
async execute ( operation ) {
if ( this . state === 'OPEN' ) {
if ( this . shouldAttemptReset ()) {
this . state = 'HALF_OPEN' ;
this . successCount = 0 ;
} else {
throw new Error ( 'Circuit breaker está OPEN. Serviço indisponível.' );
}
}
try {
const result = await operation ();
this . onSuccess ();
return result ;
} catch ( error ) {
this . onFailure ();
throw error ;
}
}
onSuccess () {
this . failureCount = 0 ;
if ( this . state === 'HALF_OPEN' ) {
this . successCount ++ ;
// Após algumas operações bem-sucedidas, fechar o circuit
if ( this . successCount >= 3 ) {
this . state = 'CLOSED' ;
console . log ( '🔄 Circuit breaker fechado. Serviço restaurado.' );
}
}
}
onFailure () {
this . failureCount ++ ;
this . lastFailureTime = Date . now ();
if ( this . failureCount >= this . failureThreshold ) {
this . state = 'OPEN' ;
console . log ( '🚫 Circuit breaker aberto. Serviço marcado como indisponível.' );
}
}
shouldAttemptReset () {
return Date . now () - this . lastFailureTime >= this . resetTimeout ;
}
}
// Uso do Circuit Breaker
const circuitBreaker = new CircuitBreaker ({
failureThreshold: 3 ,
resetTimeout: 30000
});
async function createNoteWithCircuitBreaker ( audioFile , metadata ) {
return circuitBreaker . execute ( async () => {
return client . createNote ( audioFile , metadata );
});
}
Monitoramento e Alertas
Sistema de Logs Estruturados
class ConnectVetsLogger {
static log ( level , message , data = {}) {
const logEntry = {
timestamp: new Date (). toISOString (),
level ,
message ,
service: 'connectvets-integration' ,
... data
};
console . log ( JSON . stringify ( logEntry ));
// Enviar para serviço de monitoramento
if ( level === 'ERROR' ) {
this . reportError ( logEntry );
}
}
static info ( message , data ) {
this . log ( 'INFO' , message , data );
}
static warn ( message , data ) {
this . log ( 'WARN' , message , data );
}
static error ( message , error , data = {}) {
this . log ( 'ERROR' , message , {
... data ,
error: {
message: error . message ,
stack: error . stack ,
code: error . code ,
status: error . status
}
});
}
static reportError ( logEntry ) {
// Integrar com serviços como Sentry, Datadog, etc.
// sentry.captureException(logEntry);
}
}
// Uso em operações
async function processAudio ( audioFile ) {
const startTime = Date . now ();
const operationId = generateId ();
ConnectVetsLogger . info ( 'Iniciando processamento de áudio' , {
operation_id: operationId ,
file_size: audioFile . size ,
file_type: audioFile . type
});
try {
const result = await client . createNote ( audioFile , metadata );
ConnectVetsLogger . info ( 'Processamento concluído' , {
operation_id: operationId ,
note_id: result . id ,
duration_ms: Date . now () - startTime
});
return result ;
} catch ( error ) {
ConnectVetsLogger . error ( 'Falha no processamento' , error , {
operation_id: operationId ,
duration_ms: Date . now () - startTime
});
throw error ;
}
}
class MetricsCollector {
constructor () {
this . metrics = {
requests_total: 0 ,
requests_success: 0 ,
requests_failed: 0 ,
response_times: [],
error_codes: {}
};
}
recordRequest ( duration , success , errorCode = null ) {
this . metrics . requests_total ++ ;
this . metrics . response_times . push ( duration );
// Manter apenas últimas 100 medições
if ( this . metrics . response_times . length > 100 ) {
this . metrics . response_times . shift ();
}
if ( success ) {
this . metrics . requests_success ++ ;
} else {
this . metrics . requests_failed ++ ;
if ( errorCode ) {
this . metrics . error_codes [ errorCode ] =
( this . metrics . error_codes [ errorCode ] || 0 ) + 1 ;
}
}
}
getStats () {
const responseTimes = this . metrics . response_times ;
return {
total_requests: this . metrics . requests_total ,
success_rate: this . metrics . requests_success / this . metrics . requests_total * 100 ,
avg_response_time: responseTimes . reduce (( a , b ) => a + b , 0 ) / responseTimes . length ,
p95_response_time: this . percentile ( responseTimes , 0.95 ),
error_distribution: this . metrics . error_codes
};
}
percentile ( arr , p ) {
const sorted = arr . slice (). sort (( a , b ) => a - b );
const index = Math . ceil ( sorted . length * p ) - 1 ;
return sorted [ index ];
}
}
// Uso das métricas
const metrics = new MetricsCollector ();
async function monitoredRequest ( operation ) {
const startTime = Date . now ();
try {
const result = await operation ();
const duration = Date . now () - startTime ;
metrics . recordRequest ( duration , true );
return result ;
} catch ( error ) {
const duration = Date . now () - startTime ;
metrics . recordRequest ( duration , false , error . code );
throw error ;
}
}
// Relatório periódico
setInterval (() => {
const stats = metrics . getStats ();
console . log ( '📊 Estatísticas ConnectVets:' , stats );
// Alertar se success rate < 95%
if ( stats . success_rate < 95 ) {
console . warn ( '⚠️ Alta taxa de erro detectada!' , stats );
}
}, 300000 ); // A cada 5 minutos
Testes de Error Handling
Testes Unitários
describe ( 'ConnectVets Error Handling' , () => {
let client ;
beforeEach (() => {
client = new ConnectVetsClient ( 'test_key' );
});
test ( 'deve retry em erro 500' , async () => {
const mockFetch = jest . fn ()
. mockRejectedValueOnce ( new Error ( 'Network error' ))
. mockResolvedValueOnce ({
ok: true ,
json : () => Promise . resolve ({ id: 'note_123' })
});
global . fetch = mockFetch ;
const result = await client . request ( '/notes' );
expect ( mockFetch ). toHaveBeenCalledTimes ( 2 );
expect ( result . id ). toBe ( 'note_123' );
});
test ( 'deve lançar erro em API key inválida' , async () => {
const mockFetch = jest . fn (). mockResolvedValue ({
ok: false ,
status: 401 ,
json : () => Promise . resolve ({
error: {
code: 'invalid_api_key' ,
message: 'API key inválida'
}
})
});
global . fetch = mockFetch ;
await expect ( client . request ( '/notes' )). rejects . toThrow ( 'API key inválida' );
});
test ( 'circuit breaker deve abrir após muitas falhas' , async () => {
const breaker = new CircuitBreaker ({ failureThreshold: 2 });
const failingOperation = jest . fn (). mockRejectedValue ( new Error ( 'Falha' ));
// Primeira falha
await expect ( breaker . execute ( failingOperation )). rejects . toThrow ();
expect ( breaker . state ). toBe ( 'CLOSED' );
// Segunda falha - deve abrir circuit
await expect ( breaker . execute ( failingOperation )). rejects . toThrow ();
expect ( breaker . state ). toBe ( 'OPEN' );
// Terceira tentativa deve falhar imediatamente
await expect ( breaker . execute ( failingOperation )). rejects . toThrow ( 'Circuit breaker está OPEN' );
});
});
Checklist de Error Handling
✅ Estratégias de Recovery
Próximos Passos
Onboarding Voltar ao guia de integração
Requisitos de Áudio Especificações técnicas de áudio
API Reference Documentação completa da API
Exemplos Práticos Integrações robustas funcionando