@dunx/http 3.1.3 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # @dunx/http
2
2
 
3
- `Bun.serve` adapter for [dunx](https://github.com/petarzarkov/dunx). Class-based
4
- controllers **and WebSocket gateways**, standard decorators, and no JavaScript
5
- router - Bun's native `routes` does path params and per-method dispatch in Zig.
3
+ `Bun.serve` adapter for [dunx](https://github.com/petarzarkov/dunx): class-based
4
+ controllers, **WebSocket gateways**, and standard decorators. There is no
5
+ JavaScript router - Bun's native `routes` does path params and per-method
6
+ dispatch in Zig.
6
7
 
7
8
  `Bun.serve` takes `routes` and `websocket` in one call, so both live here: one
8
9
  `listen()`, one server, one port. No `express`, no `ws`, no `socket.io`.
@@ -62,6 +63,8 @@ The guide is canonical for every row; this table is the index.
62
63
  | Middleware and guards | One extension point, `@UseGuards`, `@Roles`, `@Public` | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
63
64
  | WebSocket gateways | `@Gateway`, handlers, `PubSub`, multi-node relay | [WebSockets](../../docs/guide/09-websockets.md) |
64
65
  | Request logging | One structured entry per request, on by default | [Logging](../../docs/guide/13-logging.md) |
66
+ | Trace context | W3C `traceparent` adopted and propagated, on by default | [Logging](../../docs/guide/13-logging.md) |
67
+ | Metrics | Per-route counts and timings, off by default | [Metrics](../../docs/guide/22-metrics.md) |
65
68
  | Health and draining | `/health/live`, `/health/ready`, readiness during a rollout | [Health checks](../../docs/guide/20-health-checks.md) |
66
69
  | Throttling | `@Throttle`, `@SkipThrottle`, memory and Redis counters | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
67
70
  | Static files | `Bun.file` behind a mount, with a cache policy | [Deployment](../../docs/guide/19-deployment.md) |
@@ -89,7 +92,14 @@ from, and it may change in any release.
89
92
  - Handlers may return a `Response`, any JSON-serialisable value, or `undefined`
90
93
  for a 204.
91
94
  - Schemas, parsers and the status resolve at boot into the same closure the
92
- middleware chain folds into, so a request reads no metadata and does no lookup.
95
+ middleware chain folds into. A request reads no metadata and does no lookup.
96
+ - Every request adopts W3C Trace Context, so `traceId`, `spanId`, `parentSpanId`
97
+ and `traceFlags` reach every line it writes and `traceresponse` goes out on the
98
+ response. `requestLogging: { trace: false }` removes both;
99
+ `{ traceResponse: false }` keeps the trace and drops the header, which is ~500
100
+ ns. W3C Trace Context is the only correlation id; there is no second one.
101
+ - `metrics: true` adds per-route counts and a nanosecond histogram at +35.2 ns a
102
+ request, folded into the `.then` request logging already allocates.
93
103
 
94
104
  ## License
95
105
 
@@ -2,27 +2,32 @@
2
2
  // src/server/trace-context.ts
3
3
  var TRACEPARENT_HEADER = "traceparent";
4
4
  var TRACESTATE_HEADER = "tracestate";
5
+ var TRACERESPONSE_HEADER = "traceresponse";
5
6
  var HEX_32 = /^[0-9a-f]{32}$/;
6
7
  var HEX_16 = /^[0-9a-f]{16}$/;
7
8
  var HEX_2 = /^[0-9a-f]{2}$/;
8
9
  var ZERO_TRACE = "0".repeat(32);
9
10
  var ZERO_SPAN = "0".repeat(16);
10
11
  var SAMPLED = 1;
12
+ var DEFAULT_FLAGS = "01";
11
13
  var TRACE = Symbol.for("dunx.http.trace");
12
- var mintSpanId = () => Buffer.from(crypto.getRandomValues(new Uint8Array(8))).toString("hex");
14
+ var EXPOSE = Symbol.for("dunx.http.trace.expose");
15
+ var mint = (bytes) => crypto.getRandomValues(new Uint8Array(bytes)).toHex();
13
16
 
14
17
  class TraceContext {
15
- static adopt(req, requestId) {
18
+ static adopt(req, expose = true) {
16
19
  const inbound = TraceContext.#parse(req.headers.get(TRACEPARENT_HEADER));
17
20
  const state = req.headers.get(TRACESTATE_HEADER);
18
- const trace = {
19
- traceId: inbound?.traceId ?? requestId.replaceAll("-", ""),
20
- spanId: mintSpanId(),
21
- ...inbound === undefined ? {} : { parentSpanId: inbound.spanId },
22
- flags: inbound?.flags ?? "01",
23
- ...inbound !== undefined && state !== null ? { state } : {}
21
+ const trace = inbound === undefined ? { traceId: mint(16), spanId: mint(8), flags: DEFAULT_FLAGS } : {
22
+ traceId: inbound.traceId,
23
+ spanId: mint(8),
24
+ parentSpanId: inbound.spanId,
25
+ flags: inbound.flags,
26
+ ...state === null ? {} : { state }
24
27
  };
25
28
  req[TRACE] = trace;
29
+ if (expose)
30
+ req[EXPOSE] = true;
26
31
  return trace;
27
32
  }
28
33
  static of(req) {
@@ -31,6 +36,14 @@ class TraceContext {
31
36
  static header(trace) {
32
37
  return `00-${trace.traceId}-${trace.spanId}-${trace.flags}`;
33
38
  }
39
+ static stamp(response, req) {
40
+ const traced = req;
41
+ const trace = traced[TRACE];
42
+ if (trace !== undefined && traced[EXPOSE] === true) {
43
+ response.headers.set(TRACERESPONSE_HEADER, TraceContext.header(trace));
44
+ }
45
+ return response;
46
+ }
34
47
  static sampled(trace) {
35
48
  return (Number.parseInt(trace.flags, 16) & SAMPLED) === SAMPLED;
36
49
  }
@@ -55,4 +68,4 @@ class TraceContext {
55
68
  }
56
69
  }
57
70
 
58
- export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext };
71
+ export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TRACERESPONSE_HEADER, TraceContext };
@@ -0,0 +1,174 @@
1
+ // @bun
2
+ import {
3
+ HttpStatusCode
4
+ } from "./chunk-sz4pvqxy.js";
5
+
6
+ // src/route/marker.ts
7
+ var ROUTE = Symbol.for("dunx.route");
8
+ var CONTROLLER = Symbol.for("dunx.controller");
9
+ var defaultStatusFor = (method) => method === "POST" ? HttpStatusCode.CREATED : HttpStatusCode.OK;
10
+ var resolvePath = (path) => typeof path === "function" ? path() : path;
11
+ var markRoute = (target, meta) => {
12
+ Object.defineProperty(target, ROUTE, { value: meta, configurable: true });
13
+ };
14
+ var routeMetaOf = (value) => typeof value === "function" ? value[ROUTE] : undefined;
15
+ var markController = (target, prefix) => {
16
+ Object.defineProperty(target, CONTROLLER, {
17
+ value: prefix,
18
+ configurable: true
19
+ });
20
+ };
21
+ var prefixOf = (target) => target[CONTROLLER] ?? "";
22
+
23
+ // src/route/metadata.ts
24
+ var META = Symbol.for("dunx.meta");
25
+ var GUARDS = Symbol.for("dunx.guards");
26
+ var metaKey = (name) => ({
27
+ name,
28
+ id: Symbol(name)
29
+ });
30
+ var write = (target, key, value) => {
31
+ const record = new Map(target[META]);
32
+ record.set(key.id, value);
33
+ Object.defineProperty(target, META, { value: record, configurable: true });
34
+ };
35
+ var meta = (key, value) => (target) => {
36
+ write(target, key, value);
37
+ return target;
38
+ };
39
+ var ROLES = metaKey("roles");
40
+ var PUBLIC = metaKey("public");
41
+ var HIDDEN = metaKey("hidden");
42
+ var UNMATCHED = metaKey("unmatched");
43
+ var Roles = (...roles) => meta(ROLES, roles);
44
+ var Public = () => meta(PUBLIC, true);
45
+ var ApiHidden = () => meta(HIDDEN, true);
46
+ var UseGuards = (...guards) => (target) => {
47
+ const existing = target[GUARDS] ?? [];
48
+ const merged = Object.hasOwn(target, GUARDS) ? [...guards, ...existing] : [...existing, ...guards];
49
+ Object.defineProperty(target, GUARDS, {
50
+ value: merged,
51
+ configurable: true
52
+ });
53
+ return target;
54
+ };
55
+ var guardsOf = (target) => target[GUARDS] ?? [];
56
+ var metaOf = (target) => target[META];
57
+ var mergeMeta = (...targets) => {
58
+ const merged = new Map;
59
+ for (const target of targets) {
60
+ const record = target[META];
61
+ if (record)
62
+ for (const [id, value] of record)
63
+ merged.set(id, value);
64
+ }
65
+ return merged;
66
+ };
67
+
68
+ // src/route/discover.ts
69
+ import { markedMethods } from "@dunx/core";
70
+ var joinPath = (prefix, path) => {
71
+ const joined = `/${prefix}/${path}`.replace(/\/{2,}/g, "/");
72
+ return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
73
+ };
74
+ var discoverRoutes = (instance) => {
75
+ const klass = instance.constructor;
76
+ const prefix = prefixOf(klass);
77
+ const classGuards = guardsOf(klass);
78
+ const members = instance;
79
+ return markedMethods(Object.getPrototypeOf(instance), routeMetaOf).map(({ name, meta: meta2, value: marked }) => ({
80
+ method: meta2.method,
81
+ path: joinPath(prefix, resolvePath(meta2.path)),
82
+ controller: klass.name,
83
+ handlerName: name,
84
+ handler: members[name].bind(instance),
85
+ options: meta2.options,
86
+ meta: mergeMeta(klass, marked),
87
+ classMeta: metaOf(klass),
88
+ guards: [...classGuards, ...guardsOf(marked)]
89
+ }));
90
+ };
91
+
92
+ // src/ws/marker.ts
93
+ var HANDLER = Symbol.for("dunx.ws.handler");
94
+ var GATEWAY = Symbol.for("dunx.ws.gateway");
95
+ var HandlerKind = Object.freeze({
96
+ UPGRADE: "upgrade",
97
+ OPEN: "open",
98
+ MESSAGE: "message",
99
+ CLOSE: "close",
100
+ DRAIN: "drain",
101
+ PING: "ping",
102
+ PONG: "pong"
103
+ });
104
+ var markHandler = (target, meta2) => {
105
+ Object.defineProperty(target, HANDLER, { value: meta2, configurable: true });
106
+ };
107
+ var handlerMetaOf = (value) => typeof value === "function" ? value[HANDLER] : undefined;
108
+ var markGateway = (target, path) => {
109
+ Object.defineProperty(target, GATEWAY, { value: path, configurable: true });
110
+ };
111
+ var gatewayPathOf = (target) => target[GATEWAY] ?? "/";
112
+ var isGateway = (target) => target[GATEWAY] !== undefined;
113
+
114
+ // src/server/context.ts
115
+ var EMPTY = new Map;
116
+ var buildContext = (route) => {
117
+ const record = route.meta ?? EMPTY;
118
+ return Object.freeze({
119
+ controller: route.controller,
120
+ handler: route.handlerName,
121
+ method: route.method,
122
+ path: route.path,
123
+ parsesBody: route.options?.body !== undefined,
124
+ get: (key) => record.get(key.id)
125
+ });
126
+ };
127
+
128
+ // src/ws/discover.ts
129
+ import {
130
+ AppError,
131
+ classOf,
132
+ markedMethods as markedMethods2
133
+ } from "@dunx/core";
134
+ var normalizePath = (path) => {
135
+ const joined = `/${path}`.replace(/\/{2,}/g, "/");
136
+ return joined.length > 1 ? joined.replace(/\/$/, "") : "/";
137
+ };
138
+ var eachHandler = (start) => markedMethods2(start, handlerMetaOf);
139
+ var discoverGateway = (instance) => {
140
+ const klass = instance.constructor;
141
+ const members = instance;
142
+ return {
143
+ name: klass.name,
144
+ path: normalizePath(gatewayPathOf(klass)),
145
+ handlers: eachHandler(Object.getPrototypeOf(instance)).map(({ name, meta: meta2 }) => ({
146
+ kind: meta2.kind,
147
+ event: meta2.event,
148
+ method: name,
149
+ invoke: members[name].bind(instance)
150
+ }))
151
+ };
152
+ };
153
+ var findHandlerMethod = (ctor) => eachHandler(ctor.prototype)[0]?.name;
154
+ var discoverGateways = (modules, resolve) => {
155
+ const discovered = [];
156
+ for (const module of modules) {
157
+ for (const entry of module.options.providers ?? []) {
158
+ const candidate = classOf(entry);
159
+ if (!candidate)
160
+ continue;
161
+ if (isGateway(candidate.ctor)) {
162
+ discovered.push(discoverGateway(resolve(candidate.token)));
163
+ continue;
164
+ }
165
+ const orphan = findHandlerMethod(candidate.ctor);
166
+ if (orphan !== undefined) {
167
+ throw new AppError(`${candidate.ctor.name}.${orphan}() is a websocket handler, but ` + `${candidate.ctor.name} is not a gateway. Decorate the class with ` + "@Gateway(path), or drop the handler decorator.");
168
+ }
169
+ }
170
+ }
171
+ return discovered;
172
+ };
173
+
174
+ export { defaultStatusFor, markRoute, markController, metaKey, meta, ROLES, PUBLIC, HIDDEN, UNMATCHED, Roles, Public, ApiHidden, UseGuards, metaOf, mergeMeta, joinPath, discoverRoutes, HandlerKind, markHandler, markGateway, isGateway, discoverGateway, discoverGateways, buildContext };
@@ -16,21 +16,17 @@ export interface HttpClientOptionsInit {
16
16
  /** Sent on every request, under anything a call sets itself. */
17
17
  readonly headers?: Readonly<Record<string, string>>;
18
18
  readonly retry?: RetryOptions<unknown>;
19
- /**
20
- * Forward the inbound request id to the upstream, so one trace spans both
21
- * services. `true` uses `x-request-id`; a string names the header. Read from
22
- * `RequestContext`, so it only carries when there is a request in scope.
23
- *
24
- * @default true
25
- */
26
- readonly propagateRequestId?: boolean | string;
27
19
  /**
28
20
  * Forward W3C Trace Context upstream as `traceparent`, so the callee's spans
29
- * join this request's trace.
21
+ * join this request's trace and one trace spans both services.
30
22
  *
31
23
  * Read from `RequestContext`, so it only carries when a trace is in scope -
32
- * which means `requestLogging: { trace: true }` on the inbound side. With that
33
- * off there is nothing to send and this costs one property read.
24
+ * which the inbound side puts there unless `requestLogging: { trace: false }`
25
+ * removed it. With that off there is nothing to send and this costs one
26
+ * property read.
27
+ *
28
+ * The caller's `traceFlags` are sent as they arrived, so a trace an upstream
29
+ * sampler declined is not re-sampled at this hop.
34
30
  *
35
31
  * @default true
36
32
  */
@@ -67,7 +63,6 @@ export interface HttpClientOptionsInit {
67
63
  /** How many redirects to follow before rejecting. */
68
64
  readonly maxRedirects?: number;
69
65
  }
70
- export declare const DEFAULT_REQUEST_ID_HEADER = "x-request-id";
71
66
  /**
72
67
  * The resolved options, as a class so it is both the injection token and the type
73
68
  * a factory annotates - the same trick `RedisOptions` and `ConfigService` use.
@@ -77,7 +72,6 @@ export declare class HttpClientOptions {
77
72
  readonly timeoutMs: number;
78
73
  readonly headers: Readonly<Record<string, string>>;
79
74
  readonly retry: RetryOptions<unknown>;
80
- readonly requestIdHeader: string | undefined;
81
75
  readonly propagateTrace: boolean;
82
76
  readonly name: string | undefined;
83
77
  readonly fetchOptions: Readonly<Record<string, unknown>>;
@@ -42,8 +42,8 @@ export interface RequestConfig<TRequest = unknown, TResponse = unknown> {
42
42
  }
43
43
  type BaseOptions<TRequest, TResponse> = Omit<RequestConfig<TRequest, TResponse>, 'method' | 'url' | 'payload'>;
44
44
  /**
45
- * A `fetch` client with a per-request timeout, retry with backoff, request-id
46
- * propagation and one log line per call. `fetch` and nothing else, so there is no
45
+ * A `fetch` client with a per-request timeout, retry with backoff, W3C Trace
46
+ * Context propagation and one log line per call. `fetch` and nothing else, so there is no
47
47
  * client dependency; what it adds is the parts every caller otherwise
48
48
  * reimplements - the timeout, the retry policy, `Retry-After`, url building, and
49
49
  * a failure that says which call failed.
package/dist/client.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * `@dunx/http` does not load any of this.
7
7
  */
8
8
  export { FetchError, FetchTransportError } from './client/errors.js';
9
- export { DEFAULT_REQUEST_ID_HEADER, HttpClientOptions, type HttpClientOptionsInit, } from './client/options.js';
9
+ export { HttpClientOptions, type HttpClientOptionsInit, } from './client/options.js';
10
10
  export type { BackoffOptions, RetryOptions } from './client/retry.js';
11
11
  export { httpClient, HttpModule, type ClientTarget } from './client/module.js';
12
12
  export { HttpService, type HeaderFactory, type RequestConfig, type RequestMethod, } from './client/service.js';
package/dist/client.js CHANGED
@@ -1,25 +1,53 @@
1
1
  // @bun
2
2
  import {
3
3
  TRACEPARENT_HEADER,
4
+ TRACESTATE_HEADER,
4
5
  TraceContext
5
- } from "./chunk-25g22350.js";
6
+ } from "./chunk-3j2n1n11.js";
6
7
  import {
7
- FetchError,
8
- FetchTransportError,
9
- executeWithRetry,
10
- isJsonBody,
11
- safeStringify
12
- } from "./chunk-7xrtwbx3.js";
13
- import"./chunk-sz4pvqxy.js";
14
- // src/client/options.ts
15
- var DEFAULT_REQUEST_ID_HEADER = "x-request-id";
8
+ HttpStatusCode
9
+ } from "./chunk-sz4pvqxy.js";
10
+
11
+ // src/client/errors.ts
12
+ import { AppError } from "@dunx/core";
16
13
 
14
+ class FetchError extends AppError {
15
+ status;
16
+ statusText;
17
+ body;
18
+ response;
19
+ name = "FetchError";
20
+ constructor(status, statusText, body, response) {
21
+ super(`HTTP ${status} ${statusText} from ${response.method} ${response.url}`);
22
+ this.status = status;
23
+ this.statusText = statusText;
24
+ this.body = body;
25
+ this.response = response;
26
+ }
27
+ }
28
+ Object.defineProperty(FetchError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "override readonly status: number" }, { unresolved: "readonly statusText: string" }, { unresolved: "readonly body: unknown" }, { unresolved: `readonly response: {
29
+ readonly method: string;
30
+ readonly url: string;
31
+ readonly headers: Headers;
32
+ }` }] });
33
+
34
+ class FetchTransportError extends AppError {
35
+ response;
36
+ aborted;
37
+ name = "FetchTransportError";
38
+ constructor(response, aborted, options) {
39
+ super(`${response.method} ${response.url} failed: ${aborted ? "aborted" : "transport error"}`, options);
40
+ this.response = response;
41
+ this.aborted = aborted;
42
+ }
43
+ }
44
+ Object.defineProperty(FetchTransportError, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "readonly response: { readonly method: string; readonly url: string }" }, { unresolved: "readonly aborted: boolean" }, ErrorOptions] });
45
+ // src/client/options.ts
17
46
  class HttpClientOptions {
18
47
  baseUrl;
19
48
  timeoutMs;
20
49
  headers;
21
50
  retry;
22
- requestIdHeader;
23
51
  propagateTrace;
24
52
  name;
25
53
  fetchOptions;
@@ -30,8 +58,6 @@ class HttpClientOptions {
30
58
  this.retry = init.retry ?? {};
31
59
  this.name = init.name;
32
60
  this.propagateTrace = init.propagateTrace ?? true;
33
- const propagate = init.propagateRequestId ?? true;
34
- this.requestIdHeader = propagate === false ? undefined : propagate === true ? DEFAULT_REQUEST_ID_HEADER : propagate;
35
61
  this.fetchOptions = Object.fromEntries([
36
62
  ["proxy", init.proxy],
37
63
  ["tls", init.tls],
@@ -56,6 +82,91 @@ import {
56
82
  // src/client/service.ts
57
83
  import { Logger, RequestContext } from "@dunx/core";
58
84
  import { UrlHelper } from "@arkv/shared";
85
+
86
+ // src/client/json.ts
87
+ var safeStringify = (value) => {
88
+ const seen = new WeakSet;
89
+ return JSON.stringify(value, (_key, entry) => {
90
+ if (typeof entry === "object" && entry !== null) {
91
+ if (seen.has(entry))
92
+ return "[Circular]";
93
+ seen.add(entry);
94
+ }
95
+ return entry;
96
+ });
97
+ };
98
+ var isJsonBody = (payload) => {
99
+ if (payload === null || payload === undefined)
100
+ return false;
101
+ if (typeof payload !== "object")
102
+ return typeof payload !== "string";
103
+ return !(payload instanceof FormData || payload instanceof URLSearchParams || payload instanceof Blob || payload instanceof ArrayBuffer || payload instanceof ReadableStream || ArrayBuffer.isView(payload));
104
+ };
105
+
106
+ // src/client/retry.ts
107
+ var uniform = () => {
108
+ const buffer = new Uint32Array(1);
109
+ crypto.getRandomValues(buffer);
110
+ return (buffer[0] ?? 0) / 2 ** 32;
111
+ };
112
+ var backoffDelay = (attempt, { baseMs, power = 2, jitterMs = 1000, maxMs = 30000 }) => Math.min(baseMs * power ** attempt + uniform() * jitterMs, maxMs);
113
+ var retryAfterMs = (headers, now = Date.now()) => {
114
+ const header = headers.get("retry-after");
115
+ if (header === null)
116
+ return;
117
+ const seconds = Number(header);
118
+ if (Number.isFinite(seconds))
119
+ return Math.max(0, seconds * 1000);
120
+ const at = Date.parse(header);
121
+ return Number.isNaN(at) ? undefined : Math.max(0, at - now);
122
+ };
123
+ var isRetryableStatus = (status) => status >= HttpStatusCode.INTERNAL_SERVER_ERROR || status === HttpStatusCode.REQUEST_TIMEOUT || status === HttpStatusCode.TOO_MANY_REQUESTS;
124
+ var decide = (error, attempt, options) => {
125
+ const {
126
+ retryDelayMs = 1000,
127
+ backoff,
128
+ shouldRetryOnStatus = isRetryableStatus,
129
+ respectRetryAfter = true
130
+ } = options;
131
+ const computed = backoffDelay(attempt, { baseMs: retryDelayMs, ...backoff });
132
+ if (error instanceof FetchTransportError) {
133
+ return { retry: !error.aborted, delayMs: computed };
134
+ }
135
+ if (error instanceof FetchError) {
136
+ if (!shouldRetryOnStatus(error.status))
137
+ return { retry: false, delayMs: 0 };
138
+ const asked = respectRetryAfter ? retryAfterMs(error.response.headers) : undefined;
139
+ const maxMs = backoff?.maxMs ?? 30000;
140
+ return {
141
+ retry: true,
142
+ delayMs: asked === undefined ? computed : Math.min(asked, maxMs)
143
+ };
144
+ }
145
+ return { retry: true, delayMs: computed };
146
+ };
147
+ var executeWithRetry = async (operation, options = {}) => {
148
+ const { maxRetries = 3, onAttempt, onError, onSuccess } = options;
149
+ let lastError;
150
+ for (let attempt = 0;attempt <= maxRetries; attempt += 1) {
151
+ onAttempt?.(attempt + 1, attempt > 0);
152
+ try {
153
+ const result = await operation();
154
+ onSuccess?.(result, attempt + 1);
155
+ return result;
156
+ } catch (error) {
157
+ lastError = error;
158
+ const { retry, delayMs } = decide(error, attempt, options);
159
+ const willRetry = retry && attempt < maxRetries;
160
+ onError?.(error, attempt + 1, willRetry);
161
+ if (!willRetry)
162
+ throw error;
163
+ await Bun.sleep(delayMs);
164
+ }
165
+ }
166
+ throw lastError;
167
+ };
168
+
169
+ // src/client/service.ts
59
170
  class HttpService extends UrlHelper {
60
171
  options;
61
172
  logger;
@@ -213,19 +324,18 @@ class HttpService extends UrlHelper {
213
324
  return { body: serialised, serialised, json: true };
214
325
  }
215
326
  async send(config, url, body, serialised, accept = "application/json") {
216
- const requestId = this.options.requestIdHeader === undefined ? undefined : this.requestContext.getContext().requestId;
217
327
  const trace = this.options.propagateTrace ? this.requestContext.getContext() : undefined;
218
328
  const headers = {
219
329
  accept,
220
330
  ...serialised === "" ? {} : { "content-type": "application/json" },
221
331
  ...this.options.headers,
222
- ...requestId === undefined || this.options.requestIdHeader === undefined ? {} : { [this.options.requestIdHeader]: requestId },
223
332
  ...typeof trace?.traceId === "string" && typeof trace.spanId === "string" ? {
224
333
  [TRACEPARENT_HEADER]: TraceContext.header({
225
334
  traceId: trace.traceId,
226
335
  spanId: trace.spanId,
227
- flags: "01"
228
- })
336
+ flags: typeof trace.traceFlags === "string" ? trace.traceFlags : "01"
337
+ }),
338
+ ...typeof trace.traceState === "string" ? { [TRACESTATE_HEADER]: trace.traceState } : {}
229
339
  } : {},
230
340
  ...config.headerFactory?.({
231
341
  timestamp: Math.floor(Date.now() / 1000),
@@ -344,7 +454,6 @@ class HttpModule {
344
454
  }
345
455
  }
346
456
  export {
347
- DEFAULT_REQUEST_ID_HEADER,
348
457
  FetchError,
349
458
  FetchTransportError,
350
459
  HttpClientOptions,
package/dist/index.d.ts CHANGED
@@ -8,9 +8,9 @@ export type { CorsOptions, CorsOrigin } from './server/cors.js';
8
8
  export { defaultErrorMapper, ErrorFilter, errorMapper, HttpError, ValidationError, type ErrorHandler, type ErrorMapper, type HttpErrorOptions, type InputSource, type ValidationIssue, } from './server/errors.js';
9
9
  export { HttpFactory, type HttpApp, type HttpOptions, } from './server/factory.js';
10
10
  export { DefaultHttpOptions, HttpOptionsProvider, } from './server/options-provider.js';
11
- export { REQUEST_ID_HEADER } from './server/request-id.js';
12
- export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
11
+ export { TRACEPARENT_HEADER, TRACERESPONSE_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
13
12
  export { RequestLoggingMiddleware, type RequestLoggingOptions, } from './server/request-logging.js';
13
+ export { MetricsMiddleware, RequestMetrics, UNMATCHED_ROUTE, type HttpStatsReport, type RouteStats, } from './server/metrics.js';
14
14
  export type { Middleware, Next, RouteHandler } from './server/middleware.js';
15
15
  export type { AppSettings } from './server/settings.js';
16
16
  export { StaticFiles } from './static/files.js';