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.
Files changed (49) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/REFERENCE.md +10 -3
  3. package/dist/cli/commands/check.d.ts +8 -1
  4. package/dist/cli/commands/check.d.ts.map +1 -1
  5. package/dist/cli/commands/check.js +8 -5
  6. package/dist/cli/commands/check.js.map +1 -1
  7. package/dist/cli/commands/lint-settings.d.ts +37 -0
  8. package/dist/cli/commands/lint-settings.d.ts.map +1 -0
  9. package/dist/cli/commands/lint-settings.js +196 -0
  10. package/dist/cli/commands/lint-settings.js.map +1 -0
  11. package/dist/cli/commands/lint.d.ts.map +1 -1
  12. package/dist/cli/commands/lint.js +75 -18
  13. package/dist/cli/commands/lint.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +3 -1
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/data/db/pool.d.ts +6 -0
  18. package/dist/data/db/pool.d.ts.map +1 -1
  19. package/dist/data/db/pool.js +23 -0
  20. package/dist/data/db/pool.js.map +1 -1
  21. package/dist/infra/credits/schema.d.ts +1 -1
  22. package/dist/request/context/run-in-transaction.d.ts.map +1 -1
  23. package/dist/request/context/run-in-transaction.js +28 -15
  24. package/dist/request/context/run-in-transaction.js.map +1 -1
  25. package/dist/request/http/aborted.d.ts +7 -0
  26. package/dist/request/http/aborted.d.ts.map +1 -1
  27. package/dist/request/http/aborted.js +20 -0
  28. package/dist/request/http/aborted.js.map +1 -1
  29. package/dist/request/http/namespace.d.ts +1 -1
  30. package/dist/request/http/namespace.d.ts.map +1 -1
  31. package/dist/request/http/namespace.js +1 -1
  32. package/dist/request/http/namespace.js.map +1 -1
  33. package/dist/request/http/server.d.ts.map +1 -1
  34. package/dist/request/http/server.js +38 -9
  35. package/dist/request/http/server.js.map +1 -1
  36. package/dist/test/plugin.d.ts.map +1 -1
  37. package/dist/test/plugin.js +1 -0
  38. package/dist/test/plugin.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/cli/commands/check.ts +10 -6
  41. package/src/cli/commands/lint-settings.ts +218 -0
  42. package/src/cli/commands/lint.ts +70 -19
  43. package/src/cli/index.ts +3 -1
  44. package/src/data/db/pool.ts +29 -0
  45. package/src/request/context/run-in-transaction.ts +26 -17
  46. package/src/request/http/aborted.ts +19 -0
  47. package/src/request/http/namespace.ts +1 -1
  48. package/src/request/http/server.ts +37 -9
  49. 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, whatever the error was (a cancelled statement, the abort itself):
545
- // not a fault, nothing to send.
546
- if (abort.signal.aborted) {
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 reply
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 sendError(reply, 500, { error: { code: 'FILE_RESPONSE_INVALID', message: error.message } })
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 sendError(reply, error.httpStatus, response)
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 sendError(reply, clientStatus, response)
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 sendError(reply, 500, buildInternalErrorResponse(error, config.exposeErrorDetails))
626
+ return send(500, buildInternalErrorResponse(error, config.exposeErrorDetails))
599
627
  }
600
628
  }
601
629
 
@@ -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
  }