pipework 0.265.0 → 0.267.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/REFERENCE.md +22 -6
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/infra/alerts/sources.d.ts +3 -2
  8. package/dist/infra/alerts/sources.d.ts.map +1 -1
  9. package/dist/infra/alerts/sources.js +15 -4
  10. package/dist/infra/alerts/sources.js.map +1 -1
  11. package/dist/infra/quota/errors.d.ts +8 -2
  12. package/dist/infra/quota/errors.d.ts.map +1 -1
  13. package/dist/infra/quota/errors.js +15 -4
  14. package/dist/infra/quota/errors.js.map +1 -1
  15. package/dist/infra/quota/handle.d.ts +8 -2
  16. package/dist/infra/quota/handle.d.ts.map +1 -1
  17. package/dist/infra/quota/handle.js +37 -12
  18. package/dist/infra/quota/handle.js.map +1 -1
  19. package/dist/infra/quota/schema.d.ts +12 -0
  20. package/dist/infra/quota/schema.d.ts.map +1 -1
  21. package/dist/infra/quota/schema.js +4 -0
  22. package/dist/infra/quota/schema.js.map +1 -1
  23. package/dist/request/context/types.d.ts +2 -0
  24. package/dist/request/context/types.d.ts.map +1 -1
  25. package/dist/request/di/resolve.d.ts +1 -1
  26. package/dist/request/di/resolve.d.ts.map +1 -1
  27. package/dist/request/http/aborted.d.ts +12 -0
  28. package/dist/request/http/aborted.d.ts.map +1 -0
  29. package/dist/request/http/aborted.js +14 -0
  30. package/dist/request/http/aborted.js.map +1 -0
  31. package/dist/request/http/namespace.d.ts +3 -1
  32. package/dist/request/http/namespace.d.ts.map +1 -1
  33. package/dist/request/http/namespace.js +3 -1
  34. package/dist/request/http/namespace.js.map +1 -1
  35. package/dist/request/http/server.d.ts.map +1 -1
  36. package/dist/request/http/server.js +23 -3
  37. package/dist/request/http/server.js.map +1 -1
  38. package/dist/request/http/types.d.ts +1 -0
  39. package/dist/request/http/types.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/index.ts +1 -0
  42. package/src/infra/alerts/sources.ts +15 -4
  43. package/src/infra/quota/errors.ts +16 -5
  44. package/src/infra/quota/handle.ts +43 -14
  45. package/src/infra/quota/schema.ts +4 -0
  46. package/src/request/context/types.ts +2 -0
  47. package/src/request/di/resolve.ts +1 -1
  48. package/src/request/http/aborted.ts +15 -0
  49. package/src/request/http/namespace.ts +3 -1
  50. package/src/request/http/server.ts +21 -1
  51. package/src/request/http/types.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/request/http/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/request/http/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipework",
3
- "version": "0.265.0",
3
+ "version": "0.267.0",
4
4
  "description": "TypeScript framework for multi-tenant SaaS applications. PostgreSQL-only.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -42,6 +42,7 @@ export type { RouteScreen, ScreenedRequest, ScreenRefusal } from './request/di/t
42
42
 
43
43
  export { http } from './request/http/namespace.js'
44
44
  export { createServer } from './request/http/server.js'
45
+ export { RequestAbortedError } from './request/http/aborted.js'
45
46
  export { detectMediaType, resolveMediaType, SNIFF_BYTES } from './request/http/sniff.js'
46
47
  export type { ServerConfig, CorsConfig, SecurityConfig, RateLimitConfig,
47
48
  RateLimitClassesConfig, RateLimitClass, RateLimitWindow, RateLimitRefusal, InternalErrorFault,
@@ -396,8 +396,9 @@ function topCallers(r: Record<string, unknown>): { caller: string; sent: number;
396
396
  /**
397
397
  * Raises `quota-exhausted:<key>` while an upstream quota row (needs `quota` configured) has remaining at or
398
398
  * under `floor` (default 0) before its reset, or refused a call since it was last recorded; resolves when the
399
- * row recovers or its reset passes. `keys` limits it to those keys (a non-empty array or a RegExp). Conditions
400
- * carry `owner`, `runbook` and a context of `key`, `remaining`, `limit`, `resetAt`, `refused` and `topCallers` (the three callers that spent the most in the window, from `quota` spend counts).
399
+ * row recovers or its reset passes. It also raises while a `retry-after` pause is live (`paused_until`), reading
400
+ * "paused", not exhausted. `keys` limits it to those keys (a non-empty array or a RegExp). Conditions carry
401
+ * `owner`, `runbook` and a context of `key`, `remaining`, `limit`, `resetAt`, `refused`, `pausedUntil`, `pausedStatus` and `topCallers` (the three callers that spent the most in the window, from `quota` spend counts).
401
402
  */
402
403
  export function quotaExhaustedSource(options: { owner?: string; runbook?: string; keys?: readonly string[] | RegExp; floor?: number; severity?: AlertSeverity; database?: string } = {}): AlertSource {
403
404
  const floor = options.floor ?? 0
@@ -420,6 +421,9 @@ export function quotaExhaustedSource(options: { owner?: string; runbook?: string
420
421
  SELECT q.key, q.remaining, q."limit", q.reset_at,
421
422
  (q.remaining > ${floor} AND q.refused_at IS NOT NULL AND q.refused_at >= q.updated_at) AS refused_only,
422
423
  (q.refused_at IS NOT NULL AND q.refused_at >= q.updated_at) AS refused,
424
+ (q.paused_until > now()) AS paused,
425
+ CASE WHEN q.paused_until > now() THEN q.paused_until END AS paused_until,
426
+ CASE WHEN q.paused_until > now() THEN q.paused_status END AS paused_status,
423
427
  COALESCE(top.callers, '[]'::jsonb) AS top_callers
424
428
  FROM outbound_quota q
425
429
  LEFT JOIN LATERAL (
@@ -429,14 +433,19 @@ export function quotaExhaustedSource(options: { owner?: string; runbook?: string
429
433
  WHERE s.key = q.key AND s.window_end = q.reset_at
430
434
  ORDER BY sent + refused DESC, caller LIMIT 3) t
431
435
  ) top ON true
432
- WHERE q.reset_at > now() ${keyFilter}
433
- AND (q.remaining <= ${floor} OR (q.refused_at IS NOT NULL AND q.refused_at >= q.updated_at))
436
+ WHERE (q.reset_at > now() OR q.paused_until > now()) ${keyFilter}
437
+ AND (q.remaining <= ${floor} OR (q.refused_at IS NOT NULL AND q.refused_at >= q.updated_at) OR q.paused_until > now())
434
438
  ORDER BY q.key`,
435
439
  id: r => String(r['key']),
436
440
  message: r => {
437
441
  const remaining = Number(r['remaining'])
438
442
  const limit = r['limit'] === null ? null : Number(r['limit'])
439
443
  const head = limit === null ? `${String(r['key'])} ${String(remaining)} remaining` : `${String(r['key'])} ${String(remaining)}/${String(limit)}`
444
+ if (r['paused'] === true) {
445
+ const status = r['paused_status'] === null ? null : Number(r['paused_status'])
446
+ const left = limit === null ? `${String(remaining)} left` : `${String(remaining)}/${String(limit)} left`
447
+ return `${String(r['key'])} paused by retry-after${status !== null ? ` (status ${String(status)})` : ''} until ${untilText(new Date(r['paused_until'] as string | Date), new Date())}; ${left}`
448
+ }
440
449
  const top = topCallers(r)
441
450
  const spenders = top.length === 0 ? '' : `; top: ${top.map(c => `${c.caller} ${String(c.sent + c.refused)}`).join(', ')}`
442
451
  return `${head} until ${untilText(new Date(r['reset_at'] as string | Date), new Date())}${r['refused_only'] === true ? '; a call was refused' : ''}${spenders}`
@@ -449,6 +458,8 @@ export function quotaExhaustedSource(options: { owner?: string; runbook?: string
449
458
  resetAt: new Date(r['reset_at'] as string | Date).toISOString(),
450
459
  refused: r['refused'] === true,
451
460
  topCallers: topCallers(r),
461
+ pausedUntil: r['paused_until'] === null ? null : new Date(r['paused_until'] as string | Date).toISOString(),
462
+ pausedStatus: r['paused_status'] === null ? null : Number(r['paused_status']),
452
463
  }),
453
464
  })
454
465
  if (!(keys instanceof RegExp)) return base
@@ -2,8 +2,9 @@ import { PipeworkError } from '../../core/errors/base.js'
2
2
 
3
3
  /**
4
4
  * Thrown by `quota.call` before sending, when the upstream's recorded budget for `key`
5
- * has fallen to the reserve kept for other classes. Inside a job it is handed back like
6
- * `jobs.later` until `resetAt`.
5
+ * has fallen to the reserve kept for other classes (`reason` 'spent'), or while a `retry-after`
6
+ * pause is live (`reason` 'paused'; `resetAt` is then the end of the pause). Inside a job it is
7
+ * handed back like `jobs.later` until `resetAt`.
7
8
  */
8
9
  export class QuotaExhaustedError extends PipeworkError {
9
10
  readonly code = 'QUOTA_EXHAUSTED' as const
@@ -13,13 +14,21 @@ export class QuotaExhaustedError extends PipeworkError {
13
14
  readonly remaining: number
14
15
  readonly reserve: number
15
16
  readonly resetAt: Date
17
+ readonly reason: 'spent' | 'paused'
18
+ /** For a pause: the status of the response that carried the `retry-after`, when known. */
19
+ readonly pausedStatus: number | null
16
20
  /** Whole seconds from now to `resetAt`; sent as Retry-After and in the 429 body. */
17
21
  readonly retryAfterSeconds: number
18
22
 
19
- constructor(info: { key: string; class: string; remaining: number; reserve: number; resetAt: Date }) {
23
+ constructor(info: { key: string; class: string; remaining: number; reserve: number; resetAt: Date; reason?: 'spent' | 'paused'; pausedStatus?: number | null }) {
24
+ const paused = info.reason === 'paused'
25
+ const status = info.pausedStatus ?? null
20
26
  super(
21
- `[pipework] Quota for "${info.key}" is exhausted for class "${info.class}": ` +
22
- `${String(info.remaining)} remaining, ${String(info.reserve)} held in reserve; resets at ${info.resetAt.toISOString()}.`,
27
+ paused
28
+ ? `[pipework] Quota for "${info.key}" is paused by retry-after${status !== null ? ` (status ${String(status)})` : ''} until ${info.resetAt.toISOString()}; ` +
29
+ `${String(info.remaining)} remaining of the window.`
30
+ : `[pipework] Quota for "${info.key}" is exhausted for class "${info.class}": ` +
31
+ `${String(info.remaining)} remaining, ${String(info.reserve)} held in reserve; resets at ${info.resetAt.toISOString()}.`,
23
32
  )
24
33
  this.name = 'QuotaExhaustedError'
25
34
  this.key = info.key
@@ -27,6 +36,8 @@ export class QuotaExhaustedError extends PipeworkError {
27
36
  this.remaining = info.remaining
28
37
  this.reserve = info.reserve
29
38
  this.resetAt = info.resetAt
39
+ this.reason = info.reason ?? 'spent'
40
+ this.pausedStatus = status
30
41
  this.retryAfterSeconds = Math.max(0, Math.ceil((info.resetAt.getTime() - Date.now()) / 1000))
31
42
  }
32
43
  }
@@ -35,6 +35,10 @@ export interface QuotaState {
35
35
  readonly remaining: number
36
36
  readonly limit: number | null
37
37
  readonly resetAt: Date
38
+ /** The end of a live `retry-after` pause; null when none or it has passed. */
39
+ readonly pausedUntil: Date | null
40
+ /** The status of the response that set the pause, when known. */
41
+ readonly pausedStatus: number | null
38
42
  }
39
43
 
40
44
  export interface QuotaCallOptions<R> {
@@ -72,8 +76,8 @@ export interface QuotaSpend<K extends string = string> {
72
76
  export interface Quota<C extends string, K extends string = string> {
73
77
  /** Takes one unit of the upstream's budget for this class, sends, and records the budget from the response headers. `caller` labels the call in `spend`; without it the call counts as 'unlabelled'. */
74
78
  call<R>(target: { key: string; class: C; caller?: K }, send: () => Promise<R>, opts?: QuotaCallOptions<R>): Promise<R>
75
- /** Records the budget from headers of a call sent through the caller's own client. */
76
- record(key: string, headers: QuotaHeaderInput): Promise<void>
79
+ /** Records the budget from headers of a call sent through the caller's own client. A `retry-after` pauses the key for that many seconds; `status` is kept with the pause. */
80
+ record(key: string, headers: QuotaHeaderInput, opts?: { status?: number }): Promise<void>
77
81
  /** The recorded budget; null when there is none or its window has passed. */
78
82
  state(key: string): Promise<QuotaState | null>
79
83
  /** Calls counted per caller for the key's current and previous window. A response recorded with `record` instead of `call` is not counted. */
@@ -112,6 +116,12 @@ function headersOf(value: unknown): QuotaHeaderInput | null {
112
116
  return headers as QuotaHeaderInput
113
117
  }
114
118
 
119
+ function numberField(value: unknown, name: string): number | null {
120
+ if (typeof value !== 'object' || value === null) return null
121
+ const n = (value as Record<string, unknown>)[name]
122
+ return typeof n === 'number' && Number.isFinite(n) ? n : null
123
+ }
124
+
115
125
  function resolveDb(database: string | undefined): DB {
116
126
  const ctx = getContext()
117
127
  if (ctx === undefined) {
@@ -169,7 +179,7 @@ export function createQuota<C extends string, K extends string = string>(config:
169
179
  AND window_end <> 'infinity'::timestamptz`)
170
180
  }
171
181
 
172
- async function record(key: string, input: QuotaHeaderInput): Promise<void> {
182
+ async function record(key: string, input: QuotaHeaderInput, opts: { status?: number } = {}): Promise<void> {
173
183
  const headers = toHeaders(input)
174
184
  const budget = read(headers)
175
185
  const retryAfter = intHeader(headers, 'retry-after')
@@ -210,25 +220,32 @@ export function createQuota<C extends string, K extends string = string>(config:
210
220
  })
211
221
  }
212
222
  if (retryAfter !== null) {
223
+ // A pause of its own: remaining and reset_at of an existing row are not touched.
224
+ const secs = Math.max(0, retryAfter)
225
+ const status = opts.status !== undefined && Number.isInteger(opts.status) ? opts.status : null
213
226
  await conn.execute(sql`
214
- INSERT INTO outbound_quota (key, remaining, "limit", reset_at, updated_at)
215
- VALUES (${key}, 0, NULL, now() + make_interval(secs => ${Math.max(0, retryAfter)}), clock_timestamp())
227
+ INSERT INTO outbound_quota (key, remaining, "limit", reset_at, paused_until, paused_status, updated_at)
228
+ VALUES (${key}, 0, NULL, now() + make_interval(secs => ${secs}), now() + make_interval(secs => ${secs}), ${status}, clock_timestamp())
216
229
  ON CONFLICT (key) DO UPDATE SET
217
- remaining = 0,
218
- reset_at = GREATEST(outbound_quota.reset_at, now() + make_interval(secs => ${Math.max(0, retryAfter)})),
219
- updated_at = clock_timestamp()`)
230
+ paused_until = GREATEST(COALESCE(outbound_quota.paused_until, '-infinity'), now() + make_interval(secs => ${secs})),
231
+ paused_status = ${status}`)
220
232
  }
221
233
  }
222
234
 
223
235
  async function state(key: string): Promise<QuotaState | null> {
224
236
  const rows = await db().execute(sql`
225
- SELECT remaining, "limit", reset_at FROM outbound_quota WHERE key = ${key} AND reset_at > now()`)
237
+ SELECT remaining, "limit", reset_at,
238
+ CASE WHEN paused_until > now() THEN paused_until END AS paused_until,
239
+ CASE WHEN paused_until > now() THEN paused_status END AS paused_status
240
+ FROM outbound_quota WHERE key = ${key} AND (reset_at > now() OR paused_until > now())`)
226
241
  const row = rows[0]
227
242
  if (row === undefined) return null
228
243
  return {
229
244
  remaining: Number(row['remaining']),
230
245
  limit: row['limit'] === null ? null : Number(row['limit']),
231
246
  resetAt: new Date(row['reset_at'] as string | Date),
247
+ pausedUntil: row['paused_until'] === null ? null : new Date(row['paused_until'] as string | Date),
248
+ pausedStatus: row['paused_status'] === null ? null : Number(row['paused_status']),
232
249
  }
233
250
  }
234
251
 
@@ -240,6 +257,7 @@ export function createQuota<C extends string, K extends string = string>(config:
240
257
  WITH taken AS (
241
258
  UPDATE outbound_quota SET remaining = remaining - 1, updated_at = clock_timestamp()
242
259
  WHERE key = ${key} AND reset_at > now() AND remaining > ${reserve}
260
+ AND (paused_until IS NULL OR paused_until <= now())
243
261
  RETURNING key, reset_at)
244
262
  INSERT INTO outbound_quota_spend (key, caller, window_end, sent)
245
263
  SELECT key, ${caller}, reset_at, 1 FROM taken
@@ -250,15 +268,15 @@ export function createQuota<C extends string, K extends string = string>(config:
250
268
  const rows = await conn.execute(sql`
251
269
  WITH asked AS (
252
270
  UPDATE outbound_quota SET refused_at = clock_timestamp()
253
- WHERE key = ${key} AND reset_at > now()
254
- RETURNING key, remaining, reset_at),
271
+ WHERE key = ${key} AND (reset_at > now() OR paused_until > now())
272
+ RETURNING key, remaining, reset_at, paused_until, paused_status),
255
273
  counted AS (
256
274
  INSERT INTO outbound_quota_spend (key, caller, window_end, refused)
257
275
  SELECT key, ${caller}, reset_at, 1 FROM asked
258
276
  ON CONFLICT (key, caller, window_end) DO UPDATE SET
259
277
  refused = outbound_quota_spend.refused + 1, updated_at = clock_timestamp()
260
278
  RETURNING 1)
261
- SELECT remaining, reset_at FROM asked`)
279
+ SELECT remaining, reset_at, paused_until, paused_status FROM asked`)
262
280
  const row = rows[0]
263
281
  if (row === undefined) {
264
282
  // unknown budget: send optimistically, counted until a response places the window
@@ -269,6 +287,15 @@ export function createQuota<C extends string, K extends string = string>(config:
269
287
  sent = outbound_quota_spend.sent + 1, updated_at = clock_timestamp()`)
270
288
  return
271
289
  }
290
+ if (row['paused_until'] !== null && toDate(row['paused_until']).getTime() > Date.now()) {
291
+ throw new QuotaExhaustedError({
292
+ key, class: cls, reserve,
293
+ remaining: Number(row['remaining']),
294
+ resetAt: toDate(row['paused_until']),
295
+ reason: 'paused',
296
+ pausedStatus: row['paused_status'] === null ? null : Number(row['paused_status']),
297
+ })
298
+ }
272
299
  throw new QuotaExhaustedError({
273
300
  key, class: cls, reserve,
274
301
  remaining: Number(row['remaining']),
@@ -320,7 +347,8 @@ export function createQuota<C extends string, K extends string = string>(config:
320
347
  } catch (err) {
321
348
  const e = err as { response?: unknown } | null
322
349
  const found = headersOf(e?.response) ?? headersOf(err)
323
- if (found !== null) await record(target.key, found).catch((recordErr: unknown) => { logRecordFailure(target.key, recordErr) })
350
+ const status = numberField(e?.response, 'status') ?? numberField(err, 'status')
351
+ if (found !== null) await record(target.key, found, status !== null ? { status } : {}).catch((recordErr: unknown) => { logRecordFailure(target.key, recordErr) })
324
352
  throw err
325
353
  }
326
354
  const found = opts.headers !== undefined
@@ -330,7 +358,8 @@ export function createQuota<C extends string, K extends string = string>(config:
330
358
  return h !== null && typeof (h as QuotaHeaders).get === 'function' ? h : null
331
359
  })()
332
360
  // The call was sent: a failure to record must not reject it, or a retry would send it again.
333
- if (found !== null) await record(target.key, found).catch((recordErr: unknown) => { logRecordFailure(target.key, recordErr) })
361
+ const resultStatus = numberField(result, 'status')
362
+ if (found !== null) await record(target.key, found, resultStatus !== null ? { status: resultStatus } : {}).catch((recordErr: unknown) => { logRecordFailure(target.key, recordErr) })
334
363
  return result
335
364
  },
336
365
  }
@@ -12,6 +12,10 @@ export const outboundQuota = define('outbound_quota', {
12
12
  updatedAt: field.timestamp().default('now'),
13
13
  /** When a call was last refused against this row; it counts while it is not older than `updatedAt`. */
14
14
  refusedAt: field.timestamp().nullable(),
15
+ /** A `retry-after` pause: calls are refused until this time, whatever `remaining` says. Never touches `remaining` or `resetAt`. */
16
+ pausedUntil: field.timestamp().nullable(),
17
+ /** The status of the response whose `retry-after` set the pause, when known. */
18
+ pausedStatus: field.integer().nullable(),
15
19
  })
16
20
 
17
21
  // Calls per (key, caller, window): `window_end` is the budget row's reset_at; 'infinity' holds calls made
@@ -40,6 +40,8 @@ export interface Flow<TAuth = unknown> {
40
40
  readonly transactions: Map<string, BoundTransaction>
41
41
  /** The trace opened at this entry, or `null` when the entry ran untraced. */
42
42
  readonly trace: TraceContext | null
43
+ /** Aborted when the HTTP client closes the connection before the response is finished; pass it to outbound calls. Unset outside an HTTP request. */
44
+ readonly signal?: AbortSignal
43
45
  }
44
46
 
45
47
  /** HTTP request metadata stored in a Flow — method, path, trace ID, session ID. */
@@ -26,7 +26,7 @@ export interface ResolveOptions {
26
26
  * would be a footgun.
27
27
  */
28
28
  readonly checkpoint?: CheckpointFn
29
- /** A job's own signal, injected as the `signal` dep. */
29
+ /** A job's own signal, or an HTTP request's abort signal, injected as the `signal` dep. */
30
30
  readonly signal?: AbortSignal
31
31
  }
32
32
 
@@ -0,0 +1,15 @@
1
+ import { PipeworkError } from '../../core/errors/index.js'
2
+
3
+ /**
4
+ * The reason a request's `signal` aborts with when the client closes the
5
+ * connection before the response is finished. Not an internal error: nothing
6
+ * is sent (the socket is gone) and `onInternalError` is not called.
7
+ */
8
+ export class RequestAbortedError extends PipeworkError {
9
+ readonly code = 'REQUEST_ABORTED' as const
10
+ readonly httpStatus = 499 as const
11
+
12
+ constructor(method: string, route: string) {
13
+ super(`[pipework] The client closed the connection before the response was sent (${method} ${route}).`, { method, route })
14
+ }
15
+ }
@@ -1,5 +1,7 @@
1
1
  import { file } from './file.js'
2
2
  import { detectMediaType, resolveMediaType, SNIFF_BYTES } from './sniff.js'
3
3
 
4
- /** HTTP namespace: `http.file()` builds a file response; `http.detectMediaType()` names bytes the way uploads do; the server itself is managed by manifold.start(). */
4
+ /** HTTP namespace: `http.file()` builds a file response; `http.detectMediaType()` names bytes the way uploads do; the server itself is managed by manifold.start().
5
+ *
6
+ * A client that disconnects before its response is finished aborts the request: the in-flight statement is cancelled, the transaction rolled back and the connection released. The context's `signal` (`flow.require().signal`) aborts with a `RequestAbortedError` (499, `REQUEST_ABORTED`); pass it to the handler's own outbound calls (`fetch(url, { signal })`, a `quota.call` send). The aborted request is not an internal error: `onInternalError` is not called and nothing is sent. */
5
7
  export const http = { file, detectMediaType, resolveMediaType, SNIFF_BYTES } as const
@@ -13,6 +13,7 @@ import { createRequestContext } from '../context/index.js'
13
13
  import { executeInTransaction } from '../context/run-in-transaction.js'
14
14
  import { resolveAndExecute, parseRouteInputs, resolveHandlerDatabases, buildCheckpoint, type ResolveOptions } from '../di/resolve.js'
15
15
  import { createRateLimitClasses } from './rate-limit-classes.js'
16
+ import { RequestAbortedError } from './aborted.js'
16
17
  import { buildContext, type ContextOptions } from './middleware.js'
17
18
  import { closeTrace } from '../../trace/entry/close-trace.js'
18
19
  import { recordErrorStep } from './error-step.js'
@@ -438,12 +439,19 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
438
439
  let ctx: Flow | undefined
439
440
  let uploaded: BinaryUpload | MultipartUpload | undefined
440
441
  const track = newUploadTrack()
442
+ // Aborted when the client closes the connection before the response is finished.
443
+ const abort = new AbortController()
444
+ const onClose = (): void => {
445
+ if (!reply.raw.writableEnded && !abort.signal.aborted) abort.abort(new RequestAbortedError(method, path))
446
+ }
447
+ reply.raw.once('close', onClose)
448
+ reply.raw.once('finish', () => { reply.raw.removeListener('close', onClose) })
441
449
  // Ends the upload stream and closes the connection if the body was left unread.
442
450
  const finishUpload = (failed: boolean): void => {
443
451
  if (settleUpload(uploaded?.stream) || (failed && upload !== null)) void reply.header('connection', 'close')
444
452
  }
445
453
  try {
446
- ctx = await buildContext(req, contextOptions, handler.meta.authRequirement)
454
+ ctx = { ...(await buildContext(req, contextOptions, handler.meta.authRequirement)), signal: abort.signal }
447
455
  ;(req as FastifyRequest & { pipeworkTraceId: string | null }).pipeworkTraceId = ctx.trace?.id ?? null
448
456
  requestFlows.set(req, ctx)
449
457
 
@@ -477,6 +485,7 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
477
485
  },
478
486
  ...(rawBodyBuffer !== undefined ? { rawBody: rawBodyBuffer } : {}),
479
487
  ...(uploaded !== undefined ? { upload: uploaded } : {}),
488
+ signal: abort.signal,
480
489
  }
481
490
 
482
491
  // Fastify hands a missing body over as null; the input sees undefined.
@@ -497,6 +506,7 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
497
506
  resolveHandlerDatabases(handler, instance),
498
507
  tenantConfig,
499
508
  txTimeoutMs,
509
+ abort.signal,
500
510
  )
501
511
  result = await runInContext(ctx, () =>
502
512
  resolveAndExecute(handler, body, req.query, req.params, { ...httpOptions, checkpoint }),
@@ -508,6 +518,7 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
508
518
  tenantConfig,
509
519
  txTimeoutMs,
510
520
  () => resolveAndExecute(handler, body, req.query, req.params, httpOptions),
521
+ abort.signal,
511
522
  )
512
523
  } else {
513
524
  result = await runInContext(ctx, () => resolveAndExecute(handler, body, req.query, req.params, httpOptions))
@@ -518,6 +529,15 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
518
529
  return await sendResult(reply, req, result, handler.meta.file ?? null, `${method} ${path}`)
519
530
  } catch (error) {
520
531
  finishUpload(true)
532
+ // The client is gone, whatever the error was (a cancelled statement, the abort itself):
533
+ // not a fault, nothing to send.
534
+ if (abort.signal.aborted) {
535
+ const reason: unknown = abort.signal.reason
536
+ recordErrorStep(ctx, { method, route: path, status: 499, code: 'REQUEST_ABORTED', error: reason })
537
+ if (ctx !== undefined) void closeTrace(instance, ctx.trace, 'error')
538
+ req.log.info({ route: `${method} ${path}`, requestId: String(req.id) }, 'request aborted by client')
539
+ return reply
540
+ }
521
541
  // Records the error step, runs `refused` (the route's onRefused hook) while the
522
542
  // trace is still open, then closes the trace.
523
543
  const failWith = async (status: number, code: string, refused?: () => Promise<void>): Promise<void> => {
@@ -171,6 +171,7 @@ export interface ServerConfig {
171
171
  readyPath?: string
172
172
  healthChecks?: readonly HealthCheck[]
173
173
  transactionPerRequest?: boolean | readonly string[]
174
+ /** Bounds only opening the request's transactions (multi-database setup); it does not time the handler or its statements. A client disconnect, not this, ends a request's running work. */
174
175
  transactionTimeoutMs?: number
175
176
  /** Maximum request body size in bytes. Forwarded to Fastify (default 1 MiB). A per-route `.route(m, p, { bodyLimit })` overrides this for that route. */
176
177
  bodyLimit?: number