@tiendanube/logistics-http-client
v6.0.0
Published
Package responsible for executing http requests
Readme
Biblioteca responsável por executar requisições HTTP com mecanismos de retry, métricas e tratamento de erros integrados para os microsserviços de logística.
- Instalação
- Utilização
- Exemplo de Uso Básico
- Utilização com Configuração de Retry
- Exemplo de Uso com NestJS
- Configurações Avançadas
- Migração 5.x → 6.0.0
- Contribuindo
Migração 5.x → 6.0.0
A versão 6 muda a API de logs (logging) e adiciona duas opções de requisição. Requisições que não usam logging
não são afetadas.
Logar passou a depender do resultado da chamada. Em 5.x,
logRequestoulogResponseligavam o log. Agora é preciso dizer quando logar:// 5.x logging: { logRequest: true, logResponse: true } // 6.0.0 logging: { logOnSuccess: true, logOnError: true, logRequest: true, logResponse: true }Quem só quer o caso de erro usa
logging: { logOnError: true, logRequest: true, logResponse: true }.Uma entrada de log por chamada, não por tentativa. Em 5.x, cada tentativa gerava um log. Agora a falha só é logada quando todas as tentativas falham, e um retry que dá certo gera somente o log de sucesso.
loggingpode ser configurado no cliente. As opções da requisição vencem as do cliente, campo a campo. Um valorundefinedna requisição não apaga o valor do cliente. Oscontextsão mesclados. As listas de redact são somadas, cada uma com a lista de mesmo nome:redactRequestFieldsfinal =redactRequestFieldsdo cliente +redactRequestFieldsda requisiçãoredactResponseFieldsfinal =redactResponseFieldsdo cliente +redactResponseFieldsda requisição
As duas listas continuam separadas, como na 5.x:
redactRequestFieldsmascaraparamserequestBody, eredactResponseFieldsmascararesponseBody.O log de falha agora traz dados reais. Em 5.x,
statusCodeeresponseBodyvinhamundefinedem qualquer falha; agora são preenchidos a partir da resposta do erro, junto deerrorCodeeerrorType. OresponseBodyda falha também passa porredactResponseFields.Campos do log:
numOfAttemptsetimeoutsão novos, edurationInMspassa a ser a duração total da chamada (incluindo os retries), igual à métrica. Asserções de teste que comparam o objetoneHttpinteiro precisam incluí-los.RequestOptionsaceitaresponseTypeeresponseEncoding, repassados ao axios. Veja Download de Arquivos Binários.O log de falha usa
warnpor padrão. UseerrorLevel: 'error'para manter o nível anterior.Uma falha ao enviar a métrica não quebra mais a chamada. Em 5.x, um erro no envio da métrica (ex.: URL relativa, que
new URLnão aceita) rejeitava a chamada, mesmo com a requisição bem-sucedida, e escondia oClientErrororiginal. Agora o erro é logado comlogger.errore a chamada mantém o seu resultado.redactRequestFieldstambém mascara ospathParamsna URL logada. Em 5.x, a URL do log (neHttp.urle a mensagem) mostrava os valores reais dospathParams. Agora umpathParamcujo nome está emredactRequestFieldsaparece como[REDACTED]na URL logada. A requisição continua usando a URL real.ClientErrornão quebra mais com umresponseDataque não pode ser serializado. Em 5.x, um corpo de erro cíclico fazia o construtor doClientErrorlançar umTypeError, e a chamada rejeitava com ele. Agora a mensagem do erro mostraResponse Data: [unserializable], eresponseDatacontinua com o valor original.
O mascaramento continua igual ao da 5.x: a comparação dos nomes de campo é exata. Nada mais mudou: HttpClient,
retries, campos das métricas e DNS cache seguem iguais.
Instalação
Execute o comando:
yarn add @tiendanube/logistics-http-client❗ Atenção: é necessário estar autenticado no registry do NPM da Nuvemshop para instalar o pacote.
Utilização
A biblioteca exporta uma interface HttpClient e uma implementação para essa interface chamada LogisticsHttpClient, que recebe a injeção de uma instância de MetricHandler, um Logger e um objeto de configuração. A interface HttpClient abstrai as ações disponíveis para execução de requisições HTTP.
O cliente HTTP fornece suporte integrado para:
- Mecanismos de retry com exponential backoff
- Emissão automática de métricas para monitoramento de requisições
- Log consolidado de request e response configurável por requisição, com redaction de campos sensíveis
- Tratamento de erros com tipos de erro customizados
- Substituição de parâmetros de path para URLs dinâmicas
- Pool de conexões TCP com reuso automático para melhor performance
- Cache de DNS no nível da aplicação para reduzir latência e aumentar resiliência
Para começar a usar o cliente HTTP, você deve criar uma instância de LogisticsHttpClient fornecendo um metric handler, um logger e a configuração:
import { LogisticsHttpClient } from '@tiendanube/logistics-http-client';
import { LoggerMetricHandler } from '@tiendanube/logistics-metric-handler';
const logger = new Logger();
const metricHandler = new LoggerMetricHandler(logger);
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'my-service',
},
});❗ Atenção: É obrigatório informar os parâmetros metricHandler, logger e o objeto de configuração com metrics para a criação de uma instância de LogisticsHttpClient.
O parâmetro logger deve implementar a interface Logger, compatível com loggers comuns como pino ou o Logger do NestJS:
interface Logger {
info(obj: unknown, msg?: string, ...args: any[]): void;
error(obj: unknown, msg?: string, ...args: any[]): void;
}O logger só é efetivamente acionado quando o log está habilitado (veja Configuração de Logs). Caso contrário, ele permanece inativo.
A interface HttpClient disponibiliza os seguintes métodos HTTP:
| Método | Descrição |
| -------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| get<T>(url: string, options: RequestOptions): Promise<Response<T>> | Executa uma requisição GET |
| post<T>(url: string, data: any, options: RequestOptions): Promise<Response<T>> | Executa uma requisição POST com body |
| patch<T>(url: string, data: any, options: RequestOptions): Promise<Response<T>> | Executa uma requisição PATCH com body |
| put<T>(url: string, data: any, options: RequestOptions): Promise<Response<T>> | Executa uma requisição PUT com body |
| delete<T>(url: string, options: RequestOptions): Promise<Response<T>> | Executa uma requisição DELETE |
| request<T>(method: HttpMethod, url: string, options: RequestOptions): Promise<Response<T>> | Executa uma requisição com qualquer método HTTP |
Todos os métodos retornam uma Promise<Response<T>> onde Response contém as propriedades data e status.
Exemplo de Uso Básico
O trecho abaixo exemplifica a utilização direta da interface HttpClient para executar requisições HTTP:
import { LogisticsHttpClient } from '@tiendanube/logistics-http-client';
import { LoggerMetricHandler } from '@tiendanube/logistics-metric-handler';
const logger = new Logger();
const metricHandler = new LoggerMetricHandler(logger);
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'quotation-service',
},
});
async function fetchUserData(userId: string) {
try {
const response = await httpClient.get(`https://api.example.com/users/${userId}`, {
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
},
timeout: 5000,
validateStatus: status => status < 400,
});
return response.data;
} catch (error) {
if (error instanceof ClientError) {
console.error('HTTP Error:', error.statusCode, error.responseData); // Mais informações na seção "Tratamento de erros"
}
throw error;
}
}Utilização com Configuração de Retry
A biblioteca fornece mecanismos poderosos de retry com exponential backoff. Você pode configurar o comportamento de retry para diferentes cenários:
async function fetchWithRetry() {
const response = await httpClient.get('https://api.example.com/data', {
headers: { Authorization: 'Bearer token' },
timeout: 10000,
validateStatus: status => status < 400,
retry: {
retryOn502: true,
retryOnTimeout: true,
retryOnAbort: true,
retryOnStatusCodes: [503, 504],
maxAttempts: 5,
startingDelay: 300,
maxDelayBetweenAttempts: 2000,
timeMultiple: 2,
delayFirstAttempt: false,
jitter: 'full',
},
pathParams: {
storeId: '12345',
orderId: 'order-123',
},
isInstabilityError: (errorCode, statusCode, responseData) => {
return statusCode === 429 || responseData?.error === 'rate_limit';
},
});
return response.data;
}Exemplo de Uso com NestJS
Caso a aplicação em que esta biblioteca foi instalada seja baseada no framework NestJS, o exemplo abaixo pode ser utilizado como referência para sua correta utilização.
Idealmente a biblioteca será injetada como dependência da classe que está executando requisições HTTP. Para isso pode-se configurar a classe e um arquivo de módulo para realizar a injeção.
// quotation.service.ts
import { HttpClient, LogisticsHttpClient } from '@tiendanube/logistics-http-client';
@Injectable()
export class QuotationService {
constructor(
@Inject(LogisticsHttpClient.name)
private readonly httpClient: HttpClient,
) {}
async getExternalQuotation(storeId: string, quotationData: any) {
const response = await this.httpClient.post(
'https://api.example.com/stores/${storeId}/quotations',
quotationData,
{
headers: {
'Authorization': 'Bearer secret-token',
'Content-Type': 'application/json',
},
timeout: 15000,
validateStatus: status => status < 400,
pathParams: { storeId },
retry: {
retryOn502: true,
retryOnTimeout: true,
maxAttempts: 3,
},
},
);
return response.data;
}
}Então, é necessário configurar o módulo do Nest para injetar uma instância do HttpClient para o serviço que a utilizará:
// quotation.module.ts
import { LogisticsHttpClient, LOGISTICS_HTTP_CLIENT_METRIC_COMPONENT_NAME } from '@tiendanube/logistics-http-client';
@Module({
providers: [
{
provide: LogisticsHttpClient.name,
useFactory: async (logger: PinoLogger, config: MetricHandlerConfig) =>
new LogisticsHttpClient(
new StatsDMetricHandler(config.statsD, logger, LOGISTICS_HTTP_CLIENT_METRIC_COMPONENT_NAME),
logger,
{
metrics: {
enabled: false,
originService: 'ne-ap-com-k8s-my-service',
},
},
),
inject: [PinoLogger, metricHandlerConfig.KEY],
},
QuotationService,
],
exports: [QuotationService],
})
export class QuotationModule {}Configurações Avançadas
Tratamento de Erros
A biblioteca fornece uma classe customizada ClientError que estende o Error padrão com propriedades específicas para HTTP:
import { ClientError, ErrorType } from '@tiendanube/logistics-http-client';
try {
await httpClient.get('https://api.example.com/data', options);
} catch (error) {
if (error instanceof ClientError) {
console.log('Method:', error.method);
console.log('URL:', error.url);
console.log('Status Code:', error.statusCode);
console.log('Response Data:', error.responseData);
console.log('Error Code:', error.errorCode);
console.log('Error Type:', error.errorType);
}
}Parâmetros de Path
Use parâmetros de path para construção dinâmica de URLs:
const response = await httpClient.get('https://api.example.com/stores/${storeId}/orders/${orderId}', {
pathParams: {
storeId: '12345',
orderId: 'order-67890',
},
validateStatus: status => status < 400,
});
// URL becomes: https://api.example.com/stores/12345/orders/order-67890Download de Arquivos Binários
Para baixar um arquivo (por exemplo, uma etiqueta em PDF no S3), use responseType: 'arraybuffer'. Assim, data é
um Buffer com os bytes do arquivo. Sem essa opção, o axios converte a resposta em texto e corrompe o arquivo.
const response = await httpClient.get<Buffer>(s3Url, {
responseType: 'arraybuffer',
timeout: 5000,
validateStatus: status => status === 200,
logging: { logResponse: false, context: { trackingCode } },
});responseEncoding é repassado ao axios e só tem efeito em respostas de texto: com responseType: 'arraybuffer' o
axios o ignora.
Se o cliente estiver configurado com logResponse: true, use logResponse: false nos downloads binários. Sem isso, o
arquivo inteiro vai para o log.
Configuração de Métricas
Controle a emissão de métricas por requisição:
// Disable metrics for a specific request
await httpClient.get('https://api.example.com/health', {
validateStatus: status => status < 400,
metricEnabled: false,
});Métrica Emitida
A biblioteca emite automaticamente a métrica request (componente: logistics-http-client) para todas as requisições HTTP, a menos que desabilitada explicitamente.
Campos da métrica:
| Campo | Tipo | Descrição |
| ----------------- | ----------- | ---------------------------------------------------------- |
| duration | number | Duração total da requisição em milissegundos |
| success | boolean | Se a requisição foi bem-sucedida |
| statusCode | number | Código HTTP de resposta (200, 404, 500, etc.) |
| httpMethod | string | Método HTTP usado (GET, POST, PUT, PATCH, DELETE) |
| numOfAttempts | number | Número de tentativas realizadas (com retry) |
| baseUrl | string | URL base (origin) do servidor de destino |
| path | string | Path da requisição |
| originService | string | Nome do serviço que originou a requisição |
| externalStoreId | string | ID da loja externa (extraído automaticamente se disponível)|
| errorType | ErrorType | Tipo de erro (Instability ou Other) se houver falha |
Uma falha ao enviar a métrica não afeta a chamada: a requisição retorna o seu resultado (ou rejeita com o erro
original) e a falha é logada com logger.error, com a mensagem HTTP request metric failed {METHOD}. Esse log traz
só o erro como texto (error, ex.: TypeError: Invalid URL): a URL e o objeto de erro não são logados, porque a URL
pode conter tokens na query string.
Exemplo de uso com StatsD:
import { StatsDMetricHandler } from '@tiendanube/logistics-metric-handler';
const metricHandler = new StatsDMetricHandler(
statsDConfig,
logger,
'logistics-http-client'
);
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'quotation-service',
},
});
// Cada requisição emitirá:
// logistics-http-client.request com todos os campos acima
await httpClient.get('https://api.example.com/data', options);Essa métrica permite monitoramento completo de:
- Latência e performance de APIs externas
- Taxa de sucesso/erro por endpoint
- Impacto de retries nas requisições
- Identificação de serviços instáveis
Configuração de Logs
Além das métricas, a biblioteca pode logar o request e o response de cada chamada usando o logger informado no
construtor. O log é desabilitado por padrão e pode ser configurado no cliente (valendo para todas as requisições) e
sobrescrito por requisição, através da opção logging:
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
type LoggingOptions = {
logOnSuccess?: boolean;
logOnError?: boolean;
successLevel?: LogLevel;
errorLevel?: LogLevel;
logRequest?: boolean;
logResponse?: boolean;
redactRequestFields?: string[];
redactResponseFields?: string[];
context?: Record<string, unknown>;
};| Campo | Tipo | Padrão | Descrição |
| ---------------------- | ------------------------- | --------- | --------------------------------------------------------------------------- |
| logOnSuccess | boolean | false | Emite o log quando a chamada é bem-sucedida |
| logOnError | boolean | false | Emite o log quando todas as tentativas da chamada falham |
| successLevel | LogLevel | 'info' | Nível usado no log de sucesso |
| errorLevel | LogLevel | 'warn' | Nível usado no log de falha |
| logRequest | boolean | false | Inclui os dados do request (params e requestBody) no log |
| logResponse | boolean | false | Inclui o corpo da resposta (responseBody) no log |
| redactRequestFields | string[] | — | Campos mascarados nos params, no corpo da requisição e nos pathParams da URL logada |
| redactResponseFields | string[] | — | Campos mascarados no corpo da resposta |
| context | Record<string, unknown> | — | Campos extras do domínio de quem chama (ex.: carrierCode, trackingCode) |
O log só é emitido quando logOnSuccess (em caso de sucesso) ou logOnError (em caso de falha) for true. Para logar
somente falhas, use logOnSuccess: false e logOnError: true.
Se o logger informado não implementar o nível escolhido (warn e debug são opcionais na interface Logger), o log
é emitido em info.
Configuração no cliente (uma vez, valendo para todas as requisições):
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: { enabled: true, originService: 'my-service' },
logging: {
logOnSuccess: false,
logOnError: true,
logRequest: true,
logResponse: true,
redactRequestFields: ['password', 'token', 'email', 'phone'],
redactResponseFields: ['password', 'token', 'email', 'phone'],
},
});Sobrescrita por requisição:
await httpClient.get(labelUrl, {
validateStatus: status => status < 400,
logging: {
logResponse: false,
context: { trackingCode },
},
});As opções da requisição são combinadas com as do cliente:
- cada campo informado na requisição vence o valor do cliente; um valor
undefinednão apaga o valor do cliente; - cada lista de redact do cliente é somada à lista de mesmo nome da requisição (a requisição só adiciona
campos):
redactRequestFieldscomredactRequestFields, eredactResponseFieldscomredactResponseFields; - os campos de
contextdo cliente e da requisição são mesclados.
O que é logado:
Ao final de cada chamada é emitida uma única entrada consolidada, com a mensagem HTTP Payload {METHOD} {url}. Com
retry, a falha só é logada quando todas as tentativas falham; se um retry dá certo, só o sucesso é logado. Os metadados
são agrupados sob o nó neHttp:
| Campo | Presente quando | Descrição |
| --------------- | --------------- | ---------------------------------------------------------------- |
| method | sempre | Método HTTP |
| url | sempre | URL resolvida (após substituição de pathParams, com redactRequestFields aplicada) |
| statusCode | sempre | Código HTTP da resposta (undefined quando não houve resposta) |
| durationInMs | sempre | Duração total da chamada em milissegundos, incluindo os retries |
| numOfAttempts | sempre | Número de tentativas feitas |
| timeout | sempre | Timeout configurado para a requisição |
| errorCode | em falha | Código do erro da última tentativa (ex.: ETIMEDOUT) |
| errorType | em falha | instability ou other |
| params | logRequest | Query params (com redaction aplicado) |
| requestBody | logRequest | Body da requisição (com redaction aplicado) |
| responseBody | logResponse | Corpo da resposta (com redaction aplicado) |
| context | — | Os campos de context são espalhados dentro de neHttp |
Os nomes dos campos acima são reservados: uma chave de context com um desses nomes é sempre descartada, mesmo quando o
campo não aparece no log (ex.: responseBody com logResponse: false, ou errorType em um sucesso).
Exemplo do objeto logado em uma falha:
logger.warn(
{
neHttp: {
carrierCode: 'some-carrier',
method: 'post',
url: 'https://api.example.com/shipments',
statusCode: 422,
durationInMs: 187,
numOfAttempts: 1,
timeout: 4400,
errorCode: 'ERR_BAD_REQUEST',
errorType: 'other',
requestBody: { origin: { name: '[REDACTED]', postalCode: '11550' } },
responseBody: { error: { message: 'Invalid phone number' } },
},
},
'HTTP Payload POST https://api.example.com/shipments',
);Redaction
redactRequestFields (aplicada aos params, ao corpo da requisição e aos pathParams da URL logada) e redactResponseFields (aplicada ao corpo da
resposta) são listas de nomes de campos. Manter as duas separadas permite, por exemplo, mascarar dados pessoais que só
existem na requisição sem esconder os campos da resposta usados em diagnóstico. A comparação, igual nas duas:
- é exata e diferencia maiúsculas de minúsculas:
phonemascaraphone, mas nãoPhone,phone_numbernemphoneNumber(liste cada nome que deve ser mascarado); - percorre objetos e arrays aninhados, substituindo o valor por
[REDACTED]; - não altera valores que não são objetos simples nem arrays, como
BuffereDate; - substitui corpos de resposta em stream (
responseType: 'stream') por[STREAM], sem consumir o stream; - protege contra estruturas cíclicas (
[CIRCULAR]) e contra profundidade excessiva ([MAX_DEPTH], acima de 12 níveis).
O mascaramento só se aplica a objetos e arrays: corpos em texto (XML, HTML, JSON não parseado) e a URL são logados sem alteração.
Sem essas listas, nada é mascarado. Headers nunca são logados.
Otimizações de Performance
A biblioteca implementa otimizações automáticas de rede para maximizar performance e resiliência em ambientes de alta carga.
Cache de DNS
Por padrão, a biblioteca implementa cache de DNS no nível da aplicação usando cacheable-lookup. Isso traz diversos benefícios:
- ✅ Reduz latência: DNS lookups são cacheados por até 5 minutos (configurável)
- ✅ Aumenta resiliência: Mantém cache mesmo durante falhas temporárias de DNS
- ✅ Reduz carga: Menos consultas aos servidores DNS
- ✅ Suporta TTL: Respeita os TTLs retornados pelos servidores DNS
Configuração padrão:
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'my-service',
},
dnsCache: {
enabled: true,
maxTtlInSeconds: 300,
errorTtlInSeconds: 0.15,
},
});Desabilitar cache de DNS (não recomendado):
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'my-service',
},
dnsCache: {
enabled: false,
},
});Como funciona
O cache de DNS usa dns.resolve4/resolve6 (consulta UDP direta) para obter IPs e TTLs dos servidores DNS:
- Primeira requisição: Consulta DNS direta → Cache com TTL
- Requisições subsequentes: Retorna do cache (latência ~0ms)
- Fallback inteligente: Em caso de
ENOTFOUND, usadns.lookup(que verifica/etc/hostse cache do OS)
Isso garante compatibilidade com configurações locais (como /etc/hosts) e DNS interno do Kubernetes.
Pool de Conexões TCP
A biblioteca mantém um pool de conexões TCP reutilizáveis para cada host, eliminando a necessidade de handshakes TCP repetidos:
- ✅ Connection reuse: Até 128 conexões simultâneas por host
- ✅ Keep-alive: Conexões permanecem abertas para reutilização
- ✅ Pool management: Até 50 conexões idle mantidas no pool
Configuração padrão:
const httpClient = new LogisticsHttpClient(metricHandler, logger, {
metrics: {
enabled: true,
originService: 'my-service',
},
agent: {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 128,
maxFreeSockets: 50,
timeout: 30000,
},
});Parâmetros do Agent:
| Parâmetro | Padrão | Descrição |
| ------------------- | ------ | --------------------------------------------------------------- |
| keepAlive | true | Mantém conexões abertas para reuso |
| keepAliveMsecs | 1000 | Intervalo (ms) para enviar TCP keepalive probes |
| maxSockets | 128 | Máximo de conexões simultâneas por host |
| maxFreeSockets | 50 | Máximo de conexões idle mantidas no pool |
| timeout | 30000| Tempo (ms) que uma conexão idle permanece no pool antes de fechar |
Contribuindo
Para entender como funciona o processo de geração de novas versões para essa biblioteca, consulte a página CONTRIBUTING.
