pipework 0.274.0 → 0.276.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 +9 -0
- package/REFERENCE.md +10 -3
- package/dist/cli/commands/check.d.ts +8 -1
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +8 -5
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/lint-settings.d.ts +37 -0
- package/dist/cli/commands/lint-settings.d.ts.map +1 -0
- package/dist/cli/commands/lint-settings.js +196 -0
- package/dist/cli/commands/lint-settings.js.map +1 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +75 -18
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/data/db/pool.d.ts +6 -0
- package/dist/data/db/pool.d.ts.map +1 -1
- package/dist/data/db/pool.js +23 -0
- package/dist/data/db/pool.js.map +1 -1
- package/dist/infra/credits/schema.d.ts +1 -1
- package/dist/request/context/run-in-transaction.d.ts.map +1 -1
- package/dist/request/context/run-in-transaction.js +28 -15
- package/dist/request/context/run-in-transaction.js.map +1 -1
- package/dist/request/http/aborted.d.ts +7 -0
- package/dist/request/http/aborted.d.ts.map +1 -1
- package/dist/request/http/aborted.js +20 -0
- package/dist/request/http/aborted.js.map +1 -1
- package/dist/request/http/namespace.d.ts +1 -1
- package/dist/request/http/namespace.d.ts.map +1 -1
- package/dist/request/http/namespace.js +1 -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 +38 -9
- package/dist/request/http/server.js.map +1 -1
- package/dist/test/plugin.d.ts.map +1 -1
- package/dist/test/plugin.js +1 -0
- package/dist/test/plugin.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/check.ts +10 -6
- package/src/cli/commands/lint-settings.ts +218 -0
- package/src/cli/commands/lint.ts +70 -19
- package/src/cli/index.ts +3 -1
- package/src/data/db/pool.ts +29 -0
- package/src/request/context/run-in-transaction.ts +26 -17
- package/src/request/http/aborted.ts +19 -0
- package/src/request/http/namespace.ts +1 -1
- package/src/request/http/server.ts +37 -9
- package/src/test/plugin.ts +1 -0
|
@@ -13,7 +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
|
+
import { RequestAbortedError, isAbortCaused } from './aborted.js'
|
|
17
17
|
import { buildContext, type ContextOptions } from './middleware.js'
|
|
18
18
|
import { closeTrace } from '../../trace/entry/close-trace.js'
|
|
19
19
|
import { recordErrorStep } from './error-step.js'
|
|
@@ -39,6 +39,9 @@ import { withTenantSetting } from '../../auth/tenant/in-tenant.js'
|
|
|
39
39
|
import type { Resource } from '../fixture/types.js'
|
|
40
40
|
import { resourceToHandlers } from '../fixture/to-handlers.js'
|
|
41
41
|
|
|
42
|
+
/** The errors an aborted, injected request's raw response was destroyed with. */
|
|
43
|
+
const settledAborts = new WeakSet<object>()
|
|
44
|
+
|
|
42
45
|
/** Fastify server wrapper with route registration, handler auto-wiring, and inject() for testing. */
|
|
43
46
|
export interface PipeworkServer {
|
|
44
47
|
route(method: HttpMethod, path: string, handler: Handler<Record<string, unknown>, unknown>): void
|
|
@@ -169,6 +172,8 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
app.setErrorHandler(async (error, req, reply) => {
|
|
175
|
+
// The error an aborted request's raw response was destroyed with: not a fault, nothing to send.
|
|
176
|
+
if (typeof error === 'object' && error !== null && settledAborts.has(error)) return reply
|
|
172
177
|
const status = (error as { statusCode?: unknown }).statusCode
|
|
173
178
|
const open = requestFlows.get(req)
|
|
174
179
|
const routeOf = (): string => (req.routeOptions as { url?: string } | undefined)?.url ?? req.url.split('?')[0]!
|
|
@@ -458,6 +463,20 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
458
463
|
}
|
|
459
464
|
}
|
|
460
465
|
}
|
|
466
|
+
// An aborted request is never answered: the raw response is destroyed, so an
|
|
467
|
+
// injected caller's promise rejects with AbortError and a real socket stays closed.
|
|
468
|
+
const settleAborted = (): FastifyReply => {
|
|
469
|
+
const injected = (req.raw as { _lightMyRequest?: unknown })._lightMyRequest !== undefined
|
|
470
|
+
if (reply.raw.destroyed) return reply
|
|
471
|
+
if (!injected) {
|
|
472
|
+
reply.raw.destroy()
|
|
473
|
+
return reply
|
|
474
|
+
}
|
|
475
|
+
const abortError = new DOMException('The request was aborted', 'AbortError')
|
|
476
|
+
settledAborts.add(abortError)
|
|
477
|
+
reply.raw.destroy(abortError)
|
|
478
|
+
return reply
|
|
479
|
+
}
|
|
461
480
|
// Ends the upload stream and closes the connection if the body was left unread.
|
|
462
481
|
const finishUpload = (failed: boolean): void => {
|
|
463
482
|
if (settleUpload(uploaded?.stream) || (failed && upload !== null)) void reply.header('connection', 'close')
|
|
@@ -536,20 +555,29 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
536
555
|
result = await runInContext(ctx, () => resolveAndExecute(handler, body, req.query, req.params, httpOptions))
|
|
537
556
|
}
|
|
538
557
|
|
|
558
|
+
if (abort.signal.aborted) {
|
|
559
|
+
finishUpload(true)
|
|
560
|
+
recordErrorStep(ctx, { method, route: path, status: 499, code: 'REQUEST_ABORTED', error: abort.signal.reason })
|
|
561
|
+
void closeTrace(instance, ctx.trace, 'error')
|
|
562
|
+
req.log.info({ route: `${method} ${path}`, requestId: String(req.id) }, 'request aborted by client')
|
|
563
|
+
return settleAborted()
|
|
564
|
+
}
|
|
539
565
|
finishUpload(false)
|
|
540
566
|
void closeTrace(instance, ctx.trace, 'closed')
|
|
541
567
|
return await sendResult(reply, req, result, handler.meta.file ?? null, `${method} ${path}`)
|
|
542
568
|
} catch (error) {
|
|
543
569
|
finishUpload(true)
|
|
544
|
-
// The client is gone,
|
|
545
|
-
// not a fault
|
|
546
|
-
|
|
570
|
+
// The client is gone, so nothing is sent. What the abort caused (the abort itself, a
|
|
571
|
+
// cancelled statement) is not a fault; any other error is classified and reported as usual.
|
|
572
|
+
const aborted = abort.signal.aborted
|
|
573
|
+
if (aborted && isAbortCaused(error, abort.signal)) {
|
|
547
574
|
const reason: unknown = abort.signal.reason
|
|
548
575
|
recordErrorStep(ctx, { method, route: path, status: 499, code: 'REQUEST_ABORTED', error: reason })
|
|
549
576
|
if (ctx !== undefined) void closeTrace(instance, ctx.trace, 'error')
|
|
550
577
|
req.log.info({ route: `${method} ${path}`, requestId: String(req.id) }, 'request aborted by client')
|
|
551
|
-
return
|
|
578
|
+
return settleAborted()
|
|
552
579
|
}
|
|
580
|
+
const send = (status: number, body: unknown): FastifyReply => (aborted ? settleAborted() : sendError(reply, status, body))
|
|
553
581
|
// Records the error step, runs `refused` (the route's onRefused hook) while the
|
|
554
582
|
// trace is still open, then closes the trace.
|
|
555
583
|
const failWith = async (status: number, code: string, refused?: () => Promise<void>): Promise<void> => {
|
|
@@ -565,7 +593,7 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
565
593
|
await failWith(500, 'FILE_RESPONSE_INVALID')
|
|
566
594
|
req.log.error({ route: `${method} ${path}`, requestId: String(req.id), err: error }, 'File response refused')
|
|
567
595
|
await reportInternalError(req, error, handler)
|
|
568
|
-
return
|
|
596
|
+
return send(500, { error: { code: 'FILE_RESPONSE_INVALID', message: error.message } })
|
|
569
597
|
}
|
|
570
598
|
|
|
571
599
|
if (isPipeworkError(error)) {
|
|
@@ -581,7 +609,7 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
581
609
|
const retryAfter = response.error.retryAfterSeconds
|
|
582
610
|
if (retryAfter !== undefined) void reply.header('Retry-After', String(retryAfter))
|
|
583
611
|
if (error.httpStatus >= 500) await reportInternalError(req, error, handler)
|
|
584
|
-
return
|
|
612
|
+
return send(error.httpStatus, response)
|
|
585
613
|
}
|
|
586
614
|
|
|
587
615
|
// A malformed multipart body surfaces as a Fastify client error (4xx).
|
|
@@ -589,13 +617,13 @@ export function createServer(instance: Manifold, config: ServerConfig = {}): Pip
|
|
|
589
617
|
if (typeof clientStatus === 'number' && clientStatus >= 400 && clientStatus < 500) {
|
|
590
618
|
const response = buildClientErrorResponse(clientStatus, error)
|
|
591
619
|
await failWith(clientStatus, response.error.code, () => runRefusedHook(req, handler.meta, ctx, clientStatus, response))
|
|
592
|
-
return
|
|
620
|
+
return send(clientStatus, response)
|
|
593
621
|
}
|
|
594
622
|
|
|
595
623
|
await failWith(500, 'INTERNAL_ERROR')
|
|
596
624
|
req.log.error(error, 'Unhandled error in route handler')
|
|
597
625
|
await reportInternalError(req, error, handler)
|
|
598
|
-
return
|
|
626
|
+
return send(500, buildInternalErrorResponse(error, config.exposeErrorDetails))
|
|
599
627
|
}
|
|
600
628
|
}
|
|
601
629
|
|
package/src/test/plugin.ts
CHANGED
|
@@ -73,6 +73,7 @@ export async function beginTestIsolation(instance: Manifold, task?: WatchedTask)
|
|
|
73
73
|
' Fix: call connectionIds() on a pool opened with instance.pool.getOrCreate() or tap(), outside test isolation.',
|
|
74
74
|
))
|
|
75
75
|
},
|
|
76
|
+
async cancelBackend() { /* the one pinned test connection is never cancelled */ },
|
|
76
77
|
async close() { /* test harness manages lifecycle */ },
|
|
77
78
|
})
|
|
78
79
|
}
|