All files / src/core/observability RequestCorrelation.ts

100% Statements 4/4
100% Branches 4/4
100% Functions 1/1
100% Lines 4/4

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82                                                                                          28x                 28x                                           7x   7x      
/**
 * @class
 *
 * The one header that carries a request id from one Worker to the next.
 *
 * @remarks
 * Cloudflare gives every request that arrives from the internet a `CF-Ray`, and
 * [`CloudflareRequestMetadata`](../services/CloudflareRequestMetadata.ts) reads
 * it as the request id — the id the dashboard's own logs are keyed by, so a
 * line here and a line there can be joined without inventing anything.
 *
 * A **subrequest** gets no new one. When a Worker calls another over a Service
 * Binding, or reaches an external API, the far end sees whatever headers the
 * caller built and nothing the edge added. Without a header of its own the
 * trail stops at each hop, and a failure three Workers deep cannot be walked
 * back to the request that caused it.
 *
 * This is that header. The caller stamps its request id on the way out, the
 * callee prefers an inbound value over minting a fresh one, and one id spans
 * the whole fan-out.
 *
 * ## It is correlation, and only correlation
 *
 * The value is attacker-controlled: anyone may send `X-Request-Id`. That is
 * acceptable for joining log lines and unacceptable for anything else. It must
 * never gate access, identify a caller, or stand in for a credential — the API
 * key check in [`SecurityMiddleware`](../../middlewares/SecurityMiddleware.ts)
 * is what decides who may call, and it reads none of this. The worst a forged
 * value can do is put two unrelated requests under one id in a log query.
 *
 * @example
 * ```ts
 * const requestId = ctx.get('requestId')
 *
 * await env.NLP_WORKER.fetch('https://nlp/analyse', {
 *   method: 'POST',
 *   headers: { 'content-type': 'application/json', ...RequestCorrelation.of(requestId) },
 *   body
 * })
 * ```
 *
 * @author Bayu Dwiyan Satria
 * @version 1.2.0
 * @since 1.2.0
 */
export class RequestCorrelation {
  /**
   * Name of the header carrying the request id between Workers.
   *
   * @remarks
   * Lowercase because that is how the Workers runtime normalises header names,
   * so a lookup and a comparison agree without either side calling
   * `toLowerCase` first.
   */
  public static readonly HEADER = 'x-request-id'
 
  /**
   * Builds the correlation header for an outbound call.
   *
   * @remarks
   * Returns a plain object rather than a `Headers`, so it spreads into the
   * headers a caller was already building instead of replacing them:
   *
   * ```ts
   * headers: { 'content-type': 'application/json', ...RequestCorrelation.of(requestId) }
   * ```
   *
   * An absent id yields an empty object. A hop with nothing to propagate should
   * send no header at all rather than an empty one, which the far end would
   * read as an id and correlate against.
   *
   * @param requestId The id to propagate. Omit or pass a blank value to send
   *   nothing.
   * @returns The header as a spreadable object, empty when there is no id.
   */
  public static of(requestId?: string | null): Record<string, string> {
    const id = (requestId || '').trim()
 
    return id ? { [RequestCorrelation.HEADER]: id } : {}
  }
}