Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): adding OpenPaymentsServerRouteError #2635

Merged
merged 14 commits into from
Apr 16, 2024

Conversation

mkurapov
Copy link
Contributor

@mkurapov mkurapov commented Apr 4, 2024

Changes proposed in this pull request

  • Adding OpenPaymentsServerRouteError that is caught by a new openPaymentsServerErrorMiddleware (this middleware runs as the last middleware), which will properly handle all of errors thrown in the Open Payments Resource Server routes. This means:
    • we wont ever need to use ctx.throw in the routes, we just throw OpenPaymentsServerRouteError which will automatically be handled in the middleware
    • all logging is standardized for errors: response code + request body, other details are properly logged
    • The new @interledger/openapi package exports OpenAPIValidatorMiddlewareError, which is now also handled explicitly in the middleware

Context

Work toward #1905

Checklist

  • Related issues linked using fixes #number
  • Tests added/updated
  • Documentation added
  • Make sure that all checks pass
  • Bruno collection updated

@github-actions github-actions bot added type: tests Testing related pkg: backend Changes in the backend package. type: source Changes business logic labels Apr 4, 2024
Copy link

netlify bot commented Apr 4, 2024

Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit 57075f4
🔍 Latest deploy log https://app.netlify.com/sites/brilliant-pasca-3e80ec/deploys/661e423a0bbd850008ca34d1

@mkurapov mkurapov marked this pull request as ready for review April 9, 2024 14:30
.spyOn(tokenIntrospectionClient, 'introspect')
.mockResolvedValueOnce({ active: true, access: {} } as TokenInfo) // causes an error other than OpenPaymentsServerRouteError

expect.assertions(3)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handy expect.assertions(num) method when testing errors in try/catch to make sure the number assertions actually happened (instead of being ignored)

}

if (err instanceof OpenPaymentsServerRouteError) {
logger.info(
Copy link
Contributor Author

@mkurapov mkurapov Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info because its "expected" behaviour

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just digesting this out loud... looks like the paths are:

error is OpenPaymentsServerRouteError:

  • logs at info level with details
  • throws with status and message

error is OpenAPIValidatorMiddlewareError:

  • logs at info level with details
  • throws with status and message

anything else:

  • logs at error level with details
  • throws generic 500

Comment on lines -132 to -133
describe('get', () => {
test('returns 500 for unexpected error', async (): Promise<void> => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all properly handled in openPaymentsServerErrorMiddleware

const errorMessage =
'Outgoing payment does not have wallet address. This should not be possible.'
deps.logger.error({ outgoingPaymentId: outgoingPayment.id }, errorMessage)
throw new Error(errorMessage)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just throwing normal error here since it's something quite unexpected, and shouldn't ever actually happen

Copy link
Contributor

@BlairCurrey BlairCurrey Apr 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitely agree with your reasoning here.

this is not a suggestion, just a stackoverflow post with some interesting food for thought regarding logging errors and throwing them: https://stackoverflow.com/questions/22072343/throw-exception-vs-logging

I did some similar work previously in cleaning up errors/logs and this is triggering flashbacks. One of the things I remember was our GCP error reports would include duplicate (but not obviously duplicate) errors because of this. Could be dependent on structured logger configuration and cloud error reporting/logging services. In these scenarios I changed them to warnings so they didn't show in error reports but could still be filtered in the log viewer. I guess if the error and log message is the same the log could in theory be removed.

I'm just throwing out 1 anecdote and not claiming this is ideal. There are dissenting opinions in that post as well. Just thought it was relevant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I should also look at the middleware handling this...

I guess in this case it will:

  • log at error level
  • log again at error level in openPaymentsServerErrorMiddleware with some additional generic details
  • throw generic 500

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BlairCurrey definitely makes sense, and those order of events are correct.

I think I would also prefer just to keep only the "main" log in the middleware, but I think the only reason I included this was because I also wanted to log the outgoingPaymentId, which otherwise wouldn't get logged. I don't have a nice solution of propagating that id out to the log in the middleware (particularly for the create routes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... that seems fair. I dont think we can expect to log all the context in some central place away from where the actually error happens.

@@ -403,17 +405,26 @@ export class App {
const { resourceServerSpec, walletAddressServerSpec } =
await this.container.use('openApi')

const validatorMiddlewareOptions = {
validateRequest: true,
validateResponse: process.env.NODE_ENV !== 'production'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the createValidatorMiddleware function was doing previously, now to keep the same behaviour, it has to be stated explicitly

https://github.com/interledger/open-payments/releases/tag/%40interledger%2Fopenapi%402.0.0

Comment on lines +234 to +244
export function throwIfMissingWalletAddress(
deps: { logger: Logger },
resource: WalletAddressSubresource
): asserts resource is WalletAddressSubresource &
Required<Pick<WalletAddressSubresource, 'walletAddress'>> {
if (!resource.walletAddress) {
const errorMessage = `${resource.$modelClass.name} does not have wallet address. This should be investigated.`
deps.logger.error({ id: resource.id }, errorMessage)
throw new Error(errorMessage)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function checks whether incoming payment, quote or outgoing payment has defined walletAddress property. Maybe the log message could be less about investigation though? The only way this would happen if we don't add walletAddress wasn't added to the withGraphFetched() function when getting the resource.

Ideally this would all be resolved with types (ie. we could specify that if walletAddress is in the withGraphFetched then we can have that directly in the type -> but that would be a pretty big change, although it would be handy I think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the log message could be less about investigation though

I guess the pertinent thing is that its just (I assume) completely unexpected.

Is this based off any experience with a missing wallet address? I dont see this used so not exactly sure what the intention is and wondering why we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only to make typescript happy:

The WalletAddressSubresource model:

export abstract class WalletAddressSubresource extends BaseModel {
  ...
  public readonly walletAddressId!: string
  public walletAddress?: WalletAddress

In order to have walletAddress defined we need to include it in any withGraphFetched requests. However, Objection of course wouldn't know anything about typescript types, so we're left with walletAddress?: WalletAddress. In our routes, we need the walletAddress to be defined to do e.g.:

  ctx.body = incomingPayment.toOpenPaymentsTypeWithMethods(
    incomingPayment.walletAddress,
    streamCredentials
  )

when calling function we just assert that the wallet subresource will for sure have walletAddress defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see. For some reason I didn't see the usage until now.

Would we save ourselves some trouble by doing it in the service instead? where we are specifying .withGraphFetched('[asset, walletAddress]')? Presumably it's always a problem if we dont find the walletAddress in such cases.

Or is it acting as a typegaurd that wouldn't work if used like that? It kinda looks that way but commenting out it's usage doesn't seem to result in any errors for me.

import { OpenAPIValidatorMiddlewareError } from '@interledger/openapi'
import { WalletAddressSubresource } from './wallet_address/model'

export class OpenPaymentsServerRouteError extends Error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@njlie what do you think about having something like this in the auth package where we would define a custom GNAPError class and catch it similarily in a middleware?

@mkurapov mkurapov linked an issue Apr 11, 2024 that may be closed by this pull request
@BlairCurrey
Copy link
Contributor

BlairCurrey commented Apr 11, 2024

Is the open api request validation not working for you? I deleted the wallet address from the create incoming payment body in bruno and it makes it all the way to objection before erroring. I think the validator middleware should be picking this up first though?

image

I think the whole uncaught error path and log in the new middleware looks good though.

{
  "level": 50,
  "time": 1712859649461,
  "pid": 1,
  "hostname": "happy-life-bank-backend",
  "method": "POST",
  "path": "/incoming-payments",
  "err": {
    "type": "Error",
    "message": "undefined passed as a property in argument #0 for 'where' operation. Call skipUndefined() method to ignore the undefined values.",
    "stack": "Error: undefined passed as a property in argument #0 for 'where' operation. Call skipUndefined() method to ignore the undefined values.\n    at /home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js:164:15\n    at Array.reduce (<anonymous>)\n    at convertPlainObject (/home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js:159:27)\n    at /home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js:69:14\n    at Array.map (<anonymous>)\n    at convertArgs (/home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js:57:15)\n    at KnexOperation.getKnexArgs (/home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/ObjectionToKnexConvertingOperation.js:19:12)\n    at KnexOperation.onBuildKnex (/home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/operations/KnexOperation.js:8:59)\n    at PaginationQueryBuilder.callOperationMethod (/home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:382:33)\n    at /home/rafiki/node_modules/.pnpm/objection@3.1.4_knex@3.1.0/node_modules/objection/lib/queryBuilder/QueryBuilderOperationSupport.js:489:37"
  },
  "msg": "Received unhandled error in Open Payments request"
}

@mkurapov
Copy link
Contributor Author

@BlairCurrey
That is coming from createWalletAddressMiddleware, which is called before the validator middleware.
Unfourtunatetly I still need to update createWalletAddressMiddleware to properly throw the new error type, but I was thinking of doing it in a separate PR since this already is getting a bit too big.

If you try changing, or passing an incorrect value for a different field, you should see the correct validation error:

{"level":30,"time":1712933544211,"pid":1,"hostname":"happy-life-bank-backend","method":"POST","path":"/incoming-payments","message":"Received error validating OpenAPI request: body must NOT have additional properties: sadfdsa","status":400,"msg":"Received OpenAPI validation error when handling Open Payments request"}

Comment on lines +142 to +153
if (!(err instanceof OpenPaymentsServerRouteError)) {
throw err
}

if (err instanceof HttpError && err.status === 401) {
ctx.status = 401
ctx.message = err.message
ctx.set('WWW-Authenticate', `GNAP as_uri=${config.authServerGrantUrl}`)
} else {
ctx.set('WWW-Authenticate', `GNAP as_uri=${config.authServerGrantUrl}`)

if (!bypassError) {
throw err
}
}

await next()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no feedback, just a little retrospective: I wish I would have not added bypassError flag here and instead factored that use-case out into a separate function (kinda similar to throwIfSignatureInvalid and authenticatedStatusMiddleware). makes changes like this a lot harder to reason about. I added when implementing the public incoming payment.

Changes look fine though... getting public payment works as expected.

Comment on lines -129 to -145

test('returns 500 for unexpected error', async (): Promise<void> => {
const incomingPaymentService = await deps.use('incomingPaymentService')
jest
.spyOn(incomingPaymentService, 'getWalletAddressPage')
.mockRejectedValueOnce(new Error('unexpected'))
const ctx = setup<ListContext>({
reqOpts: {
headers: { Accept: 'application/json' }
},
walletAddress
})
await expect(incomingPaymentRoutes.list(ctx)).rejects.toMatchObject({
status: 500,
message: `Error trying to list incoming payments`
})
})
Copy link
Contributor

@BlairCurrey BlairCurrey Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we removing this because the case is already covered by the general error handling middleware test? or something else? I realize the rejects assertions here are no longer correct but I'm just trying to verify the new behavior is tested.

BlairCurrey
BlairCurrey previously approved these changes Apr 12, 2024
@mkurapov mkurapov merged commit 99df005 into main Apr 16, 2024
30 checks passed
@mkurapov mkurapov deleted the mk/1905/rs-route-errors branch April 16, 2024 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pkg: backend Changes in the backend package. type: source Changes business logic type: tests Testing related
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Standardize Open Payment errors
3 participants