@atproto/xrpc-server 0.6.4 → 0.7.1

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)
@@ -280,59 +282,48 @@ export class Server {
280
282
  }
281
283
 
282
284
  // run the handler
283
- 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)
284
305
 
285
- if (isHandlerError(outputUnvalidated)) {
286
- throw XRPCError.fromError(outputUnvalidated)
287
- }
306
+ res.status(200)
307
+ setHeaders(res, output)
288
308
 
289
- if (outputUnvalidated && isHandlerPipeThrough(outputUnvalidated)) {
290
- // set headers
291
- if (outputUnvalidated?.headers) {
292
- Object.entries(outputUnvalidated.headers).forEach(([name, val]) => {
293
- res.header(name, val)
294
- })
295
- }
296
- res
297
- .header('Content-Type', outputUnvalidated.encoding)
298
- .status(200)
299
- .send(Buffer.from(outputUnvalidated.buffer))
300
- return
301
- }
302
-
303
- if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
304
- // validate response
305
- const output = validateResOutput(outputUnvalidated)
306
- // set headers
307
- if (output?.headers) {
308
- Object.entries(output.headers).forEach(([name, val]) => {
309
- res.header(name, val)
310
- })
311
- }
312
- // send response
313
309
  if (
314
- output?.encoding === 'application/json' ||
315
- output?.encoding === 'json'
310
+ output.encoding === 'application/json' ||
311
+ output.encoding === 'json'
316
312
  ) {
317
313
  const json = lexToJson(output.body)
318
- res.status(200).json(json)
319
- } else if (output?.body instanceof Readable) {
314
+ res.json(json)
315
+ } else if (output.body instanceof Readable) {
320
316
  res.header('Content-Type', output.encoding)
321
- res.status(200)
322
- res.once('error', (err) => res.destroy(err))
323
- forwardStreamErrors(output.body, res)
324
- output.body.pipe(res)
325
- } else if (output) {
326
- res
327
- .header('Content-Type', output.encoding)
328
- .status(200)
329
- .send(
330
- 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
331
324
  ? Buffer.from(output.body)
332
325
  : output.body,
333
- )
334
- } else {
335
- res.status(200).end()
326
+ )
336
327
  }
337
328
  }
338
329
  } catch (err: unknown) {
@@ -364,7 +355,7 @@ export class Server {
364
355
  // authenticate request
365
356
  const auth = await config.auth?.({ req })
366
357
  if (isHandlerError(auth)) {
367
- throw XRPCError.fromError(auth)
358
+ throw XRPCError.fromHandlerError(auth)
368
359
  }
369
360
  // validate request
370
361
  let params = decodeQueryParams(def, getQueryParams(req.url))
@@ -481,30 +472,18 @@ export class Server {
481
472
  }
482
473
  }
483
474
 
484
- function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
485
- return handlerSuccess.safeParse(v).success
486
- }
487
-
488
- function isHandlerPipeThrough(v: HandlerOutput): v is HandlerPipeThrough {
489
- if (v === null || typeof v !== 'object') {
490
- return false
491
- }
492
- if (!isString(v['encoding']) || !(v['buffer'] instanceof ArrayBuffer)) {
493
- return false
494
- }
495
- if (v['headers'] !== undefined) {
496
- if (v['headers'] === null || typeof v['headers'] !== 'object') {
497
- return false
498
- }
499
- if (!Object.values(v['headers']).every(isString)) {
500
- 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)
501
483
  }
502
484
  }
503
- return true
504
485
  }
505
486
 
506
- const isString = (val: unknown): val is string => typeof val === 'string'
507
-
508
487
  const kRequestLocals = Symbol('requestLocals')
509
488
 
510
489
  function createLocalsMiddleware(nsid: string): RequestHandler {
@@ -525,7 +504,7 @@ function createAuthMiddleware(verifier: AuthVerifier): RequestHandler {
525
504
  try {
526
505
  const result = await verifier({ req, res })
527
506
  if (isHandlerError(result)) {
528
- throw XRPCError.fromError(result)
507
+ throw XRPCError.fromHandlerError(result)
529
508
  }
530
509
  const locals: RequestLocals = req[kRequestLocals]
531
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
  }