@atproto/xrpc-server 0.6.3 → 0.7.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/src/server.ts CHANGED
@@ -1,14 +1,4 @@
1
- import { Readable } from 'stream'
2
- import express, {
3
- Application,
4
- Express,
5
- Router,
6
- Request,
7
- Response,
8
- ErrorRequestHandler,
9
- NextFunction,
10
- RequestHandler,
11
- } from 'express'
1
+ import { check, schema } from '@atproto/common'
12
2
  import {
13
3
  LexiconDoc,
14
4
  Lexicons,
@@ -17,31 +7,45 @@ import {
17
7
  LexXrpcQuery,
18
8
  LexXrpcSubscription,
19
9
  } from '@atproto/lexicon'
20
- import { check, forwardStreamErrors, schema } from '@atproto/common'
10
+ import express, {
11
+ Application,
12
+ ErrorRequestHandler,
13
+ Express,
14
+ NextFunction,
15
+ Request,
16
+ RequestHandler,
17
+ Response,
18
+ Router,
19
+ } from 'express'
20
+ import { Readable } from 'node:stream'
21
+ import { pipeline } from 'node:stream/promises'
22
+
23
+ import log from './logger'
24
+ import { consumeMany } from './rate-limiter'
21
25
  import { ErrorFrame, Frame, MessageFrame, XrpcStreamServer } from './stream'
22
26
  import {
23
- XRPCHandler,
24
- XRPCError,
25
- InvalidRequestError,
26
- HandlerOutput,
27
- HandlerSuccess,
28
- handlerSuccess,
29
- XRPCHandlerConfig,
30
- MethodNotImplementedError,
31
- HandlerAuth,
32
27
  AuthVerifier,
28
+ HandlerAuth,
29
+ HandlerPipeThrough,
30
+ HandlerSuccess,
31
+ InternalServerError,
32
+ InvalidRequestError,
33
33
  isHandlerError,
34
+ isHandlerPipeThroughBuffer,
35
+ isHandlerPipeThroughStream,
36
+ isShared,
37
+ MethodNotImplementedError,
34
38
  Options,
35
- XRPCStreamHandlerConfig,
36
- XRPCStreamHandler,
37
39
  Params,
38
- InternalServerError,
39
- XRPCReqContext,
40
- RateLimiterI,
41
40
  RateLimiterConsume,
42
- isShared,
41
+ RateLimiterI,
43
42
  RateLimitExceededError,
44
- HandlerPipeThrough,
43
+ XRPCError,
44
+ XRPCHandler,
45
+ XRPCHandlerConfig,
46
+ XRPCReqContext,
47
+ XRPCStreamHandler,
48
+ XRPCStreamHandlerConfig,
45
49
  } from './types'
46
50
  import {
47
51
  decodeQueryParams,
@@ -49,8 +53,6 @@ import {
49
53
  validateInput,
50
54
  validateOutput,
51
55
  } from './util'
52
- import log from './logger'
53
- import { consumeMany } from './rate-limiter'
54
56
 
55
57
  export function createServer(lexicons?: LexiconDoc[], options?: Options) {
56
58
  return new Server(lexicons, options)
@@ -243,8 +245,8 @@ export class Server {
243
245
  validateInput(nsid, def, req, routeOpts, this.lex)
244
246
  const validateResOutput =
245
247
  this.options.validateResponse === false
246
- ? (output?: HandlerSuccess) => output
247
- : (output?: HandlerSuccess) =>
248
+ ? null
249
+ : (output: undefined | HandlerSuccess) =>
248
250
  validateOutput(nsid, def, output, this.lex)
249
251
  const assertValidXrpcParams = (params: unknown) =>
250
252
  this.lex.assertValidXrpcParams(nsid, params)
@@ -263,11 +265,6 @@ export class Server {
263
265
  }
264
266
  const input = validateReqInput(req)
265
267
 
266
- if (input?.body instanceof Readable) {
267
- // If the body stream errors at any time, abort the request
268
- input.body.once('error', next)
269
- }
270
-
271
268
  const locals: RequestLocals = req[kRequestLocals]
272
269
 
273
270
  const reqCtx: XRPCReqContext = {
@@ -285,59 +282,48 @@ export class Server {
285
282
  }
286
283
 
287
284
  // run the handler
288
- const outputUnvalidated = await routeCfg.handler(reqCtx)
285
+ const output = await routeCfg.handler(reqCtx)
286
+
287
+ if (!output) {
288
+ validateResOutput?.(output)
289
+ res.status(200)
290
+ res.end()
291
+ } else if (isHandlerPipeThroughStream(output)) {
292
+ setHeaders(res, output)
293
+ res.status(200)
294
+ res.header('Content-Type', output.encoding)
295
+ await pipeline(output.stream, res)
296
+ } else if (isHandlerPipeThroughBuffer(output)) {
297
+ setHeaders(res, output)
298
+ res.status(200)
299
+ res.header('Content-Type', output.encoding)
300
+ res.end(output.buffer)
301
+ } else if (isHandlerError(output)) {
302
+ next(XRPCError.fromError(output))
303
+ } else {
304
+ validateResOutput?.(output)
289
305
 
290
- if (isHandlerError(outputUnvalidated)) {
291
- throw XRPCError.fromError(outputUnvalidated)
292
- }
306
+ res.status(200)
307
+ setHeaders(res, output)
293
308
 
294
- if (outputUnvalidated && isHandlerPipeThrough(outputUnvalidated)) {
295
- // set headers
296
- if (outputUnvalidated?.headers) {
297
- Object.entries(outputUnvalidated.headers).forEach(([name, val]) => {
298
- res.header(name, val)
299
- })
300
- }
301
- res
302
- .header('Content-Type', outputUnvalidated.encoding)
303
- .status(200)
304
- .send(Buffer.from(outputUnvalidated.buffer))
305
- return
306
- }
307
-
308
- if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
309
- // validate response
310
- const output = validateResOutput(outputUnvalidated)
311
- // set headers
312
- if (output?.headers) {
313
- Object.entries(output.headers).forEach(([name, val]) => {
314
- res.header(name, val)
315
- })
316
- }
317
- // send response
318
309
  if (
319
- output?.encoding === 'application/json' ||
320
- output?.encoding === 'json'
310
+ output.encoding === 'application/json' ||
311
+ output.encoding === 'json'
321
312
  ) {
322
313
  const json = lexToJson(output.body)
323
- res.status(200).json(json)
324
- } else if (output?.body instanceof Readable) {
314
+ res.json(json)
315
+ } else if (output.body instanceof Readable) {
325
316
  res.header('Content-Type', output.encoding)
326
- res.status(200)
327
- res.once('error', (err) => res.destroy(err))
328
- forwardStreamErrors(output.body, res)
329
- output.body.pipe(res)
330
- } else if (output) {
331
- res
332
- .header('Content-Type', output.encoding)
333
- .status(200)
334
- .send(
335
- output.body instanceof Uint8Array
317
+ await pipeline(output.body, res)
318
+ } else {
319
+ res.header('Content-Type', output.encoding)
320
+ res.send(
321
+ Buffer.isBuffer(output.body)
322
+ ? output.body
323
+ : output.body instanceof Uint8Array
336
324
  ? Buffer.from(output.body)
337
325
  : output.body,
338
- )
339
- } else {
340
- res.status(200).end()
326
+ )
341
327
  }
342
328
  }
343
329
  } catch (err: unknown) {
@@ -369,7 +355,7 @@ export class Server {
369
355
  // authenticate request
370
356
  const auth = await config.auth?.({ req })
371
357
  if (isHandlerError(auth)) {
372
- throw XRPCError.fromError(auth)
358
+ throw XRPCError.fromHandlerError(auth)
373
359
  }
374
360
  // validate request
375
361
  let params = decodeQueryParams(def, getQueryParams(req.url))
@@ -486,30 +472,18 @@ export class Server {
486
472
  }
487
473
  }
488
474
 
489
- function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
490
- return handlerSuccess.safeParse(v).success
491
- }
492
-
493
- function isHandlerPipeThrough(v: HandlerOutput): v is HandlerPipeThrough {
494
- if (v === null || typeof v !== 'object') {
495
- return false
496
- }
497
- if (!isString(v['encoding']) || !(v['buffer'] instanceof ArrayBuffer)) {
498
- return false
499
- }
500
- if (v['headers'] !== undefined) {
501
- if (v['headers'] === null || typeof v['headers'] !== 'object') {
502
- return false
503
- }
504
- if (!Object.values(v['headers']).every(isString)) {
505
- return false
475
+ function setHeaders(
476
+ res: Response,
477
+ result: HandlerSuccess | HandlerPipeThrough,
478
+ ) {
479
+ const { headers } = result
480
+ if (headers) {
481
+ for (const [name, val] of Object.entries(headers)) {
482
+ if (val != null) res.header(name, val)
506
483
  }
507
484
  }
508
- return true
509
485
  }
510
486
 
511
- const isString = (val: unknown): val is string => typeof val === 'string'
512
-
513
487
  const kRequestLocals = Symbol('requestLocals')
514
488
 
515
489
  function createLocalsMiddleware(nsid: string): RequestHandler {
@@ -530,7 +504,7 @@ function createAuthMiddleware(verifier: AuthVerifier): RequestHandler {
530
504
  try {
531
505
  const result = await verifier({ req, res })
532
506
  if (isHandlerError(result)) {
533
- throw XRPCError.fromError(result)
507
+ throw XRPCError.fromHandlerError(result)
534
508
  }
535
509
  const locals: RequestLocals = req[kRequestLocals]
536
510
  locals.auth = result
package/src/types.ts CHANGED
@@ -1,12 +1,14 @@
1
- import { IncomingMessage } from 'http'
2
- import express from 'express'
3
- import { isHttpError } from 'http-errors'
4
- import zod from 'zod'
5
1
  import {
6
2
  ResponseType,
7
- ResponseTypeStrings,
8
3
  ResponseTypeNames,
4
+ ResponseTypeStrings,
5
+ XRPCError as XRPCClientError,
9
6
  } from '@atproto/xrpc'
7
+ import express from 'express'
8
+ import { IncomingMessage } from 'http'
9
+ import { isHttpError } from 'http-errors'
10
+ import { Readable } from 'stream'
11
+ import zod from 'zod'
10
12
 
11
13
  export type CatchallHandler = (
12
14
  req: express.Request,
@@ -46,18 +48,40 @@ export const handlerAuth = zod.object({
46
48
  })
47
49
  export type HandlerAuth = zod.infer<typeof handlerAuth>
48
50
 
51
+ export const headersSchema = zod.record(zod.string())
52
+
49
53
  export const handlerSuccess = zod.object({
50
54
  encoding: zod.string(),
51
55
  body: zod.any(),
52
- headers: zod.record(zod.string()).optional(),
56
+ headers: headersSchema.optional(),
53
57
  })
54
58
  export type HandlerSuccess = zod.infer<typeof handlerSuccess>
55
59
 
56
- export const handlerPipeThrough = zod.object({
60
+ export const handlerPipeThroughBuffer = zod.object({
57
61
  encoding: zod.string(),
58
- buffer: zod.instanceof(ArrayBuffer),
59
- headers: zod.record(zod.string()).optional(),
62
+ buffer: zod.instanceof(Buffer),
63
+ headers: headersSchema.optional(),
60
64
  })
65
+
66
+ export type HandlerPipeThroughBuffer = zod.infer<
67
+ typeof handlerPipeThroughBuffer
68
+ >
69
+
70
+ export const handlerPipeThroughStream = zod.object({
71
+ encoding: zod.string(),
72
+ stream: zod.instanceof(Readable),
73
+ headers: headersSchema.optional(),
74
+ })
75
+
76
+ export type HandlerPipeThroughStream = zod.infer<
77
+ typeof handlerPipeThroughStream
78
+ >
79
+
80
+ export const handlerPipeThrough = zod.union([
81
+ handlerPipeThroughBuffer,
82
+ handlerPipeThroughStream,
83
+ ])
84
+
61
85
  export type HandlerPipeThrough = zod.infer<typeof handlerPipeThrough>
62
86
 
63
87
  export const handlerError = zod.object({
@@ -186,8 +210,9 @@ export class XRPCError extends Error {
186
210
  public type: ResponseType,
187
211
  public errorMessage?: string,
188
212
  public customErrorName?: string,
213
+ options?: ErrorOptions,
189
214
  ) {
190
- super(errorMessage)
215
+ super(errorMessage, options)
191
216
  }
192
217
 
193
218
  get payload() {
@@ -208,22 +233,36 @@ export class XRPCError extends Error {
208
233
  return ResponseTypeStrings[this.type]
209
234
  }
210
235
 
211
- static fromError(error: unknown) {
212
- if (error instanceof XRPCError) {
213
- return error
236
+ static fromError(cause: unknown): XRPCError {
237
+ if (cause instanceof XRPCError) {
238
+ return cause
214
239
  }
215
- let resultErr: XRPCError
216
- if (isHttpError(error)) {
217
- resultErr = new XRPCError(error.status, error.message, error.name)
218
- } else if (isHandlerError(error)) {
219
- resultErr = new XRPCError(error.status, error.message, error.error)
220
- } else if (error instanceof Error) {
221
- resultErr = new InternalServerError(error.message)
222
- } else {
223
- resultErr = new InternalServerError('Unexpected internal server error')
240
+
241
+ if (cause instanceof XRPCClientError) {
242
+ return new XRPCError(cause.status, cause.message, cause.error, { cause })
243
+ }
244
+
245
+ if (isHttpError(cause)) {
246
+ return new XRPCError(cause.status, cause.message, cause.name, { cause })
224
247
  }
225
- resultErr.cause = error
226
- return resultErr
248
+
249
+ if (isHandlerError(cause)) {
250
+ return this.fromHandlerError(cause)
251
+ }
252
+
253
+ if (cause instanceof Error) {
254
+ return new InternalServerError(cause.message, undefined, { cause })
255
+ }
256
+
257
+ return new InternalServerError(
258
+ 'Unexpected internal server error',
259
+ undefined,
260
+ { cause },
261
+ )
262
+ }
263
+
264
+ static fromHandlerError(err: HandlerError): XRPCError {
265
+ return new XRPCError(err.status, err.message, err.error, { cause: err })
227
266
  }
228
267
  }
229
268
 
@@ -237,21 +276,67 @@ export function isHandlerError(v: unknown): v is HandlerError {
237
276
  )
238
277
  }
239
278
 
279
+ export function isHandlerPipeThroughBuffer(
280
+ v: HandlerOutput,
281
+ ): v is HandlerPipeThroughBuffer {
282
+ // We only need to discriminate between possible HandlerOutput values
283
+ return v['buffer'] !== undefined
284
+ }
285
+
286
+ export function isHandlerPipeThroughStream(
287
+ v: HandlerOutput,
288
+ ): v is HandlerPipeThroughStream {
289
+ // We only need to discriminate between possible HandlerOutput values
290
+ return v['stream'] !== undefined
291
+ }
292
+
240
293
  export class InvalidRequestError extends XRPCError {
241
- constructor(errorMessage?: string, customErrorName?: string) {
242
- super(ResponseType.InvalidRequest, errorMessage, customErrorName)
294
+ constructor(
295
+ errorMessage?: string,
296
+ customErrorName?: string,
297
+ options?: ErrorOptions,
298
+ ) {
299
+ super(ResponseType.InvalidRequest, errorMessage, customErrorName, options)
300
+ }
301
+
302
+ [Symbol.hasInstance](instance: unknown): boolean {
303
+ return (
304
+ instance instanceof XRPCError &&
305
+ instance.type === ResponseType.InvalidRequest
306
+ )
243
307
  }
244
308
  }
245
309
 
246
310
  export class AuthRequiredError extends XRPCError {
247
- constructor(errorMessage?: string, customErrorName?: string) {
248
- super(ResponseType.AuthRequired, errorMessage, customErrorName)
311
+ constructor(
312
+ errorMessage?: string,
313
+ customErrorName?: string,
314
+ options?: ErrorOptions,
315
+ ) {
316
+ super(ResponseType.AuthRequired, errorMessage, customErrorName, options)
317
+ }
318
+
319
+ [Symbol.hasInstance](instance: unknown): boolean {
320
+ return (
321
+ instance instanceof XRPCError &&
322
+ instance.type === ResponseType.AuthRequired
323
+ )
249
324
  }
250
325
  }
251
326
 
252
327
  export class ForbiddenError extends XRPCError {
253
- constructor(errorMessage?: string, customErrorName?: string) {
254
- super(ResponseType.Forbidden, errorMessage, customErrorName)
328
+ constructor(
329
+ errorMessage?: string,
330
+ customErrorName?: string,
331
+ options?: ErrorOptions,
332
+ ) {
333
+ super(ResponseType.Forbidden, errorMessage, customErrorName, options)
334
+ }
335
+
336
+ [Symbol.hasInstance](instance: unknown): boolean {
337
+ return (
338
+ instance instanceof XRPCError && instance.type === ResponseType.Forbidden
339
+ )
255
340
  }
256
341
  }
257
342
 
@@ -260,37 +345,120 @@ export class RateLimitExceededError extends XRPCError {
260
345
  public status: RateLimiterStatus,
261
346
  errorMessage?: string,
262
347
  customErrorName?: string,
348
+ options?: ErrorOptions,
263
349
  ) {
264
- super(ResponseType.RateLimitExceeded, errorMessage, customErrorName)
350
+ super(
351
+ ResponseType.RateLimitExceeded,
352
+ errorMessage,
353
+ customErrorName,
354
+ options,
355
+ )
356
+ }
357
+
358
+ [Symbol.hasInstance](instance: unknown): boolean {
359
+ return (
360
+ instance instanceof XRPCError &&
361
+ instance.type === ResponseType.RateLimitExceeded
362
+ )
265
363
  }
266
364
  }
267
365
 
268
366
  export class InternalServerError extends XRPCError {
269
- constructor(errorMessage?: string, customErrorName?: string) {
270
- super(ResponseType.InternalServerError, errorMessage, customErrorName)
367
+ constructor(
368
+ errorMessage?: string,
369
+ customErrorName?: string,
370
+ options?: ErrorOptions,
371
+ ) {
372
+ super(
373
+ ResponseType.InternalServerError,
374
+ errorMessage,
375
+ customErrorName,
376
+ options,
377
+ )
378
+ }
379
+
380
+ [Symbol.hasInstance](instance: unknown): boolean {
381
+ return (
382
+ instance instanceof XRPCError &&
383
+ instance.type === ResponseType.InternalServerError
384
+ )
271
385
  }
272
386
  }
273
387
 
274
388
  export class UpstreamFailureError extends XRPCError {
275
- constructor(errorMessage?: string, customErrorName?: string) {
276
- super(ResponseType.UpstreamFailure, errorMessage, customErrorName)
389
+ constructor(
390
+ errorMessage?: string,
391
+ customErrorName?: string,
392
+ options?: ErrorOptions,
393
+ ) {
394
+ super(ResponseType.UpstreamFailure, errorMessage, customErrorName, options)
395
+ }
396
+
397
+ [Symbol.hasInstance](instance: unknown): boolean {
398
+ return (
399
+ instance instanceof XRPCError &&
400
+ instance.type === ResponseType.UpstreamFailure
401
+ )
277
402
  }
278
403
  }
279
404
 
280
405
  export class NotEnoughResourcesError extends XRPCError {
281
- constructor(errorMessage?: string, customErrorName?: string) {
282
- super(ResponseType.NotEnoughResources, errorMessage, customErrorName)
406
+ constructor(
407
+ errorMessage?: string,
408
+ customErrorName?: string,
409
+ options?: ErrorOptions,
410
+ ) {
411
+ super(
412
+ ResponseType.NotEnoughResources,
413
+ errorMessage,
414
+ customErrorName,
415
+ options,
416
+ )
417
+ }
418
+
419
+ [Symbol.hasInstance](instance: unknown): boolean {
420
+ return (
421
+ instance instanceof XRPCError &&
422
+ instance.type === ResponseType.NotEnoughResources
423
+ )
283
424
  }
284
425
  }
285
426
 
286
427
  export class UpstreamTimeoutError extends XRPCError {
287
- constructor(errorMessage?: string, customErrorName?: string) {
288
- super(ResponseType.UpstreamTimeout, errorMessage, customErrorName)
428
+ constructor(
429
+ errorMessage?: string,
430
+ customErrorName?: string,
431
+ options?: ErrorOptions,
432
+ ) {
433
+ super(ResponseType.UpstreamTimeout, errorMessage, customErrorName, options)
434
+ }
435
+
436
+ [Symbol.hasInstance](instance: unknown): boolean {
437
+ return (
438
+ instance instanceof XRPCError &&
439
+ instance.type === ResponseType.UpstreamTimeout
440
+ )
289
441
  }
290
442
  }
291
443
 
292
444
  export class MethodNotImplementedError extends XRPCError {
293
- constructor(errorMessage?: string, customErrorName?: string) {
294
- super(ResponseType.MethodNotImplemented, errorMessage, customErrorName)
445
+ constructor(
446
+ errorMessage?: string,
447
+ customErrorName?: string,
448
+ options?: ErrorOptions,
449
+ ) {
450
+ super(
451
+ ResponseType.MethodNotImplemented,
452
+ errorMessage,
453
+ customErrorName,
454
+ options,
455
+ )
456
+ }
457
+
458
+ [Symbol.hasInstance](instance: unknown): boolean {
459
+ return (
460
+ instance instanceof XRPCError &&
461
+ instance.type === ResponseType.MethodNotImplemented
462
+ )
295
463
  }
296
464
  }