@tiendanube/logistics-http-client
v5.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
- Contribuindo
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.external-service.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://external-api.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-67890Configuraçã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 |
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.external.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 requisição usando o logger informado no construtor. O log é desabilitado por padrão e é configurado por requisição, através da opção logging no RequestOptions:
type LoggingOptions = {
logRequest?: boolean;
logResponse?: boolean;
redactRequestFields?: string[];
redactResponseFields?: string[];
};| Campo | Tipo | Descrição |
| ---------------------- | ---------- | ------------------------------------------------------------------------------- |
| logRequest | boolean | Quando true, inclui os dados do request (params e requestBody) no log |
| logResponse | boolean | Quando true, inclui o corpo da resposta (responseBody) no log |
| redactRequestFields | string[] | Campos a serem mascarados nos dados do request |
| redactResponseFields | string[] | Campos a serem mascarados no corpo da resposta |
O log só é emitido quando logRequest ou logResponse for true.
Exemplo:
await httpClient.post('https://api.example.com/login', body, {
validateStatus: status => status < 400,
logging: {
logRequest: true,
logResponse: true,
redactRequestFields: ['password', 'token'],
redactResponseFields: ['accessToken'],
},
});O que é logado:
Ao final da requisição (com sucesso ou falha) é emitida uma única entrada info consolidada, com a mensagem HTTP Payload {METHOD} {url}. 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) |
| statusCode | sempre | Código HTTP da resposta (undefined em falha sem resposta) |
| durationInMs | sempre | Duração da requisição em milissegundos |
| params | logRequest | Query params (com redaction aplicado) |
| requestBody | logRequest | Body da requisição (com redaction aplicado) |
| responseBody | logResponse | Corpo da resposta (com redaction aplicado) |
Exemplo do objeto logado:
logger.info(
{
neHttp: {
method: 'post',
url: 'https://api.example.com/login',
statusCode: 200,
durationInMs: 42,
params: { foo: 'bar' },
requestBody: { username: 'someUser', password: '[REDACTED]' },
responseBody: { id: 1, accessToken: '[REDACTED]' },
},
},
'HTTP Payload POST https://api.example.com/login',
);❗ Atenção: por questões de segurança, os headers da requisição nunca são logados (para evitar vazamento de tokens de autenticação). Os campos listados em redactRequestFields/redactResponseFields são substituídos por '[REDACTED]' — inclusive em objetos aninhados e em itens de arrays. Ainda assim, params, requestBody e responseBody podem conter dados sensíveis: habilite logRequest/logResponse com cautela.
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.
