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.
- package/CHANGELOG.md +8 -0
- package/REFERENCE.md +22 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/infra/alerts/sources.d.ts +3 -2
- package/dist/infra/alerts/sources.d.ts.map +1 -1
- package/dist/infra/alerts/sources.js +15 -4
- package/dist/infra/alerts/sources.js.map +1 -1
- package/dist/infra/quota/errors.d.ts +8 -2
- package/dist/infra/quota/errors.d.ts.map +1 -1
- package/dist/infra/quota/errors.js +15 -4
- package/dist/infra/quota/errors.js.map +1 -1
- package/dist/infra/quota/handle.d.ts +8 -2
- package/dist/infra/quota/handle.d.ts.map +1 -1
- package/dist/infra/quota/handle.js +37 -12
- package/dist/infra/quota/handle.js.map +1 -1
- package/dist/infra/quota/schema.d.ts +12 -0
- package/dist/infra/quota/schema.d.ts.map +1 -1
- package/dist/infra/quota/schema.js +4 -0
- package/dist/infra/quota/schema.js.map +1 -1
- package/dist/request/context/types.d.ts +2 -0
- package/dist/request/context/types.d.ts.map +1 -1
- package/dist/request/di/resolve.d.ts +1 -1
- package/dist/request/di/resolve.d.ts.map +1 -1
- package/dist/request/http/aborted.d.ts +12 -0
- package/dist/request/http/aborted.d.ts.map +1 -0
- package/dist/request/http/aborted.js +14 -0
- package/dist/request/http/aborted.js.map +1 -0
- package/dist/request/http/namespace.d.ts +3 -1
- package/dist/request/http/namespace.d.ts.map +1 -1
- package/dist/request/http/namespace.js +3 -1
- package/dist/request/http/namespace.js.map +1 -1
- package/dist/request/http/server.d.ts.map +1 -1
- package/dist/request/http/server.js +23 -3
- package/dist/request/http/server.js.map +1 -1
- package/dist/request/http/types.d.ts +1 -0
- package/dist/request/http/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/infra/alerts/sources.ts +15 -4
- package/src/infra/quota/errors.ts +16 -5
- package/src/infra/quota/handle.ts +43 -14
- package/src/infra/quota/schema.ts +4 -0
- package/src/request/context/types.ts +2 -0
- package/src/request/di/resolve.ts +1 -1
- package/src/request/http/aborted.ts +15 -0
- package/src/request/http/namespace.ts +3 -1
- package/src/request/http/server.ts +21 -1
- 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
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.
|
|
400
|
-
*
|
|
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
|
|
6
|
-
* `
|
|
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
|
-
|
|
22
|
-
|
|
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 => ${
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|