-
Notifications
You must be signed in to change notification settings - Fork 89
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
Conversation
✅ Deploy Preview for brilliant-pasca-3e80ec canceled.
|
.spyOn(tokenIntrospectionClient, 'introspect') | ||
.mockResolvedValueOnce({ active: true, access: {} } as TokenInfo) // causes an error other than OpenPaymentsServerRouteError | ||
|
||
expect.assertions(3) |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
describe('get', () => { | ||
test('returns 500 for unexpected error', async (): Promise<void> => { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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
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) | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
# Conflicts: # pnpm-lock.yaml
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? 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"
}
|
@BlairCurrey If you try changing, or passing an incorrect value for a different field, you should see the correct validation error:
|
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() |
There was a problem hiding this comment.
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.
|
||
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` | ||
}) | ||
}) |
There was a problem hiding this comment.
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.
# Conflicts: # pnpm-lock.yaml
Changes proposed in this pull request
OpenPaymentsServerRouteError
that is caught by a newopenPaymentsServerErrorMiddleware
(this middleware runs as the last middleware), which will properly handle all of errors thrown in the Open Payments Resource Server routes. This means:ctx.throw
in the routes, we just throwOpenPaymentsServerRouteError
which will automatically be handled in the middleware@interledger/openapi
package exportsOpenAPIValidatorMiddlewareError
, which is now also handled explicitly in the middlewareContext
Work toward #1905
Checklist
fixes #number