Skip to content

Commit

Permalink
Improve xrpc server error handling (#1597)
Browse files Browse the repository at this point in the history
improve xrpc server error handling
  • Loading branch information
dholms authored Sep 14, 2023
1 parent 578757c commit 1c42157
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 14 deletions.
21 changes: 14 additions & 7 deletions packages/xrpc-server/src/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class RateLimiter implements RateLimiterI {
async consume(
ctx: XRPCReqContext,
opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn },
): Promise<RateLimiterStatus | null> {
): Promise<RateLimiterStatus | RateLimitExceededError | null> {
if (
this.byPassSecret &&
ctx.req.header('x-ratelimit-bypass') === this.byPassSecret
Expand All @@ -82,7 +82,7 @@ export class RateLimiter implements RateLimiterI {
// yes this library rejects with a res not an error
if (err instanceof RateLimiterRes) {
const status = formatLimiterStatus(this.limiter, err)
throw new RateLimitExceededError(status)
return new RateLimitExceededError(status)
} else {
if (this.failClosed) {
throw err
Expand Down Expand Up @@ -119,12 +119,18 @@ export const formatLimiterStatus = (
export const consumeMany = async (
ctx: XRPCReqContext,
fns: RateLimiterConsume[],
): Promise<void> => {
if (fns.length === 0) return
): Promise<RateLimiterStatus | RateLimitExceededError | null> => {
if (fns.length === 0) return null
const results = await Promise.all(fns.map((fn) => fn(ctx)))
const tightestLimit = getTightestLimit(results)
if (tightestLimit !== null) {
if (tightestLimit === null) {
return null
} else if (tightestLimit instanceof RateLimitExceededError) {
setResHeaders(ctx, tightestLimit.status)
return tightestLimit
} else {
setResHeaders(ctx, tightestLimit)
return tightestLimit
}
}

Expand All @@ -142,11 +148,12 @@ export const setResHeaders = (
}

export const getTightestLimit = (
resps: (RateLimiterStatus | null)[],
): RateLimiterStatus | null => {
resps: (RateLimiterStatus | RateLimitExceededError | null)[],
): RateLimiterStatus | RateLimitExceededError | null => {
let lowest: RateLimiterStatus | null = null
for (const resp of resps) {
if (resp === null) continue
if (resp instanceof RateLimitExceededError) return resp
if (lowest === null || resp.remainingPoints < lowest.remainingPoints) {
lowest = resp
}
Expand Down
23 changes: 18 additions & 5 deletions packages/xrpc-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
RateLimiterI,
RateLimiterConsume,
isShared,
RateLimitExceededError,
} from './types'
import {
decodeQueryParams,
Expand Down Expand Up @@ -247,7 +248,10 @@ export class Server {

// handle rate limits
if (consumeRateLimit) {
await consumeRateLimit(reqCtx)
const result = await consumeRateLimit(reqCtx)
if (result instanceof RateLimitExceededError) {
return next(result)
}
}

// run the handler
Expand Down Expand Up @@ -475,14 +479,23 @@ function createAuthMiddleware(verifier: AuthVerifier): RequestHandler {
const errorMiddleware: ErrorRequestHandler = function (err, req, res, next) {
const locals: RequestLocals | undefined = req[kRequestLocals]
const methodSuffix = locals ? ` method ${locals.nsid}` : ''
if (err instanceof XRPCError) {
log.error(err, `error in xrpc${methodSuffix}`)
} else {
const xrpcError = XRPCError.fromError(err)
if (xrpcError instanceof InternalServerError) {
// log trace for unhandled exceptions
log.error(err, `unhandled exception in xrpc${methodSuffix}`)
} else {
// do not log trace for known xrpc errors
log.error(
{
status: xrpcError.type,
message: xrpcError.message,
name: xrpcError.customErrorName,
},
`error in xrpc${methodSuffix}`,
)
}
if (res.headersSent) {
return next(err)
}
const xrpcError = XRPCError.fromError(err)
return res.status(xrpcError.type).json(xrpcError.payload)
}
4 changes: 2 additions & 2 deletions packages/xrpc-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export interface RateLimiterI {
export type RateLimiterConsume = (
ctx: XRPCReqContext,
opts?: { calcKey?: CalcKeyFn; calcPoints?: CalcPointsFn },
) => Promise<RateLimiterStatus | null>
) => Promise<RateLimiterStatus | RateLimitExceededError | null>

export type RateLimiterCreator = (opts: {
keyPrefix: string
Expand Down Expand Up @@ -224,7 +224,7 @@ export class ForbiddenError extends XRPCError {

export class RateLimitExceededError extends XRPCError {
constructor(
status: RateLimiterStatus,
public status: RateLimiterStatus,
errorMessage?: string,
customErrorName?: string,
) {
Expand Down

0 comments on commit 1c42157

Please sign in to comment.