<h1>logistics-http-client</h1>

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.

<h1>Índice</h1>

-   [Instalação](#instalação)
-   [Utilização](#utilização)
-   [Exemplo de Uso Básico](#exemplo-de-uso-básico)
-   [Utilização com Configuração de Retry](#utilização-com-configuração-de-retry)
-   [Exemplo de Uso com NestJS](#exemplo-de-uso-com-nestjs)
-   [Configurações Avançadas](#configurações-avançadas)
    -   [Tratamento de Erros](#tratamento-de-erros)
    -   [Parâmetros de Path](#parâmetros-de-path)
    -   [Configuração de Métricas](#configuração-de-métricas)
        -   [Métrica Emitida](#métrica-emitida)
    -   [Configuração de Logs](#configuração-de-logs)
    -   [Otimizações de Performance](#otimizações-de-performance)
-   [Contribuindo](#contribuindo)

# Instalação

Execute o comando:

```bash
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:

```ts
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:

```ts
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](#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:

```ts
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:

```ts
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.

```ts
// 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á:

```ts
// 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:

```ts
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:

```ts
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-67890
```

## Configuração de Métricas

Controle a emissão de métricas por requisição:

```ts
// 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**:

```ts
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`:

```ts
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**:

```ts
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:

```ts
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**:

```ts
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):

```ts
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:

1. **Primeira requisição**: Consulta DNS direta → Cache com TTL
2. **Requisições subsequentes**: Retorna do cache (latência ~0ms)
3. **Fallback inteligente**: Em caso de `ENOTFOUND`, usa `dns.lookup` (que verifica `/etc/hosts` e 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**:

```ts
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](./CONTRIBUTING.md).
