Опирается на правила: R-HDR-1..4 и R-HDR-X1 из REST API Style Guide → раздел Заголовки и трассировка.

Важно знать

  • Стандартные заголовки — по назначению (Content-Type, Authorization, Location, ETag).
  • Кастомные — с доменным префиксом, без X- (устарел RFC 6648).
  • Idempotency-Key — через @Headers('Idempotency-Key') для POST с retry-безопасностью.
  • traceparent@opentelemetry/instrumentation-http прокидывает автоматически.
  • traceId из traceparent используется в теле ошибки RFC 9457.
  • Locationres.location(...) при 201 Created.

NestJS работает с заголовками через декораторы @Headers, @Header, res.setHeader(). Трассировка через OpenTelemetry SDK — автоматически без ручного кода в контроллерах.

Стандартные заголовки

R-HDR-1:

HeaderНазначениеГде в NestJS
Content-Typeтип телаres.type(...) или Express default
Acceptожидаемый тип ответачитать через @Headers('accept')
Authorizationаутентификация@Headers('authorization') или Guard
LocationURL созданного ресурсаres.location('/api/v1/orders/...')
ETagверсия ресурсаres.setHeader('ETag', '"abc123"')
Cache-Controlкешированиеres.setHeader('Cache-Control', 'private, max-age=60')
@Get(':id')
async findOne(
  @Param('id') id: string,
  @Headers('if-none-match') ifNoneMatch: string,
  @Res({ passthrough: true }) res: Response,
) {
  const order = await this.ordersService.findOne(id);
  const etag = `"${order.version}"`;

  if (ifNoneMatch === etag) {
    res.status(304).end();
    return;
  }

  res.setHeader('ETag', etag);
  return order;
}

Location при создании

R-RSP-3:

@Post()
async create(
  @Body() dto: CreateOrderDto,
  @Res({ passthrough: true }) res: Response,
) {
  const order = await this.ordersService.create(dto);
  res.location(`/api/v1/orders/${order.orderId}`);
  return order;
}

Кастомные заголовки с доменным префиксом

R-HDR-2: единый префикс для всех сервисов проекта.

@Post()
async create(
  @Headers('shop-request-id') requestId: string,    // ✓ доменный префикс
  @Headers('shop-client-version') clientVersion: string,
  @Body() dto: CreateOrderDto,
) {
  this.logger.log({ requestId, clientVersion }, 'create order');
  return this.ordersService.create(dto);
}
// ✗ X- префикс — устарел по RFC 6648
@Headers('x-request-id')

// ✓
@Headers('shop-request-id')

Shop-Request-Id vs traceparent:

  • Shop-Request-Id — идентификатор конкретного запроса от клиента (дедупликация, логирование).
  • traceparent — идентификатор всей цепочки вызовов (distributed trace).

Idempotency-Key

R-HDR-3: безопасный retry для POST.

export class IdempotencyGuard implements CanActivate {
  constructor(private readonly idempotencyService: IdempotencyService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest<Request>();
    const key = req.headers['idempotency-key'] as string;

    if (!key) {
      throw new BadRequestException('Idempotency-Key header is required');
    }

    const existing = await this.idempotencyService.find(key);
    if (existing) {
      const res = context.switchToHttp().getResponse<Response>();
      res.status(existing.status).json(existing.body);
      return false;
    }

    return true;
  }
}

@Post()
@UseGuards(IdempotencyGuard)
async create(
  @Headers('idempotency-key') key: string,
  @Body() dto: CreateOrderDto,
): Promise<OrderResponse> {
  return this.ordersService.create(dto, key);
}

Контракт:

  • Клиент генерирует Idempotency-Key (UUID v4) один раз на бизнес-операцию.
  • Повторный POST с тем же ключом → сервер возвращает первый результат.
  • Другой payload с тем же ключом → 409 Conflict.

traceparent — W3C Trace Context

R-HDR-4: через @opentelemetry/instrumentation-http.

// main.ts — инициализация до NestFactory.create
import { NodeSDK } from '@opentelemetry/sdk-node';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';

const sdk = new NodeSDK({
  instrumentations: [new HttpInstrumentation()],
});
sdk.start();

После этого traceparent извлекается из входящего запроса автоматически — trace-id доступен через OpenTelemetry API:

// telemetry/trace-context.ts
import { context, trace } from '@opentelemetry/api';

export function getTraceId(): string {
  const span = trace.getActiveSpan();
  return span?.spanContext().traceId ?? '';
}

Использование traceId в теле ошибки RFC 9457 (см. Ошибки):

// edge/problem.ts
export function sendProblem(res: Response, status: number, title: string, detail: string): void {
  res.status(status).type('application/problem+json').json({
    title,
    status,
    detail,
    traceId: getTraceId(),   // из traceparent
  });
}

Структура traceparent:

00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01
│  │                                │                │
│  trace-id (32 hex)                parent-id (16)   flags
version

Правила обработки:

  1. Клиент прислал traceparent → использовать его trace-id, создать новый parent-id.
  2. Клиент не прислал → HttpInstrumentation генерирует новый traceparent на входе.

Что запрещено

АнтипаттернПравилоЧто взамен
@Headers('x-request-id') кастомныйR-HDR-X1@Headers('shop-request-id')
@Headers('x-tenant-id')R-HDR-X1@Headers('shop-tenant-id')
Authorization без Bearer для JWTR-HDR-1Bearer eyJhbGci...
Idempotency-Key для GET-запросовR-HDR-3только POST/PATCH
Самописный Tracking-Id вместо traceparentR-HDR-4W3C standard + OTel
trace-id 16 hex вместо 32R-HDR-432 hex
Location без /api/v1/ префиксаR-RSP-3полный путь

Куда дальше

  • Ошибки RFC 9457 — traceId в теле ошибки.
  • JSON и формат ответов — Location при 201.
  • Rate limiting, файлы, deprecation — Retry-After, Sunset.
  • REST API Style Guide (нормативно) — формулировки.