-
Notifications
You must be signed in to change notification settings - Fork 22
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: access-api proxy.js has configurable options.catchInvocationError, by default catches HTTPError -> error result w/ status=502 #366
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,50 @@ | |
import * as Ucanto from '@ucanto/interface' | ||
import * as Client from '@ucanto/client' | ||
|
||
const BadGatewayHTTPErrorResult = { | ||
/** | ||
* Given unknown error, detect whether it is an upstream HTTPError. | ||
* If so, return a result object indicating generic 'Bad Gateway'. | ||
* Otherwise, if error is not a bad gateway error, return undefined | ||
* | ||
* @param {unknown} error - error encountered when proxying a ucanto invocation to an upstream url | ||
*/ | ||
catch(error) { | ||
if (!error || typeof error !== 'object') { | ||
return | ||
} | ||
const status = 'status' in error ? Number(error.status) : undefined | ||
const isServerError = status !== undefined && status >= 500 && status < 600 | ||
if (!isServerError) { | ||
return | ||
} | ||
return { | ||
error: true, | ||
status: 502, | ||
statusText: 'Bad Gateway', | ||
'x-proxy-error': error, | ||
} | ||
}, | ||
} | ||
|
||
/** | ||
* default catchInvocationError value for createProxyHandler. | ||
* It catches `HTTPError` errors to an error result with status=502 and statusText='Bad Gateway' | ||
* | ||
* @param {unknown} error | ||
*/ | ||
function defaultCatchInvocationError(error) { | ||
const badGatewayResult = BadGatewayHTTPErrorResult.catch(error) | ||
if (badGatewayResult) { | ||
return badGatewayResult | ||
} | ||
throw error | ||
} | ||
|
||
/** | ||
* @template {Ucanto.ConnectionView<any>} [Connection=Ucanto.ConnectionView<any>] | ||
* @param {object} options | ||
* @param {(error: unknown) => Promise<unknown>} [options.catchInvocationError] - catches any error that comes from invoking the proxy invocation on the connection. If it returns a value, that value will be the proxied invocation result. | ||
* @param {{ default: Connection, [K: Ucanto.UCAN.DID]: Connection }} options.connections | ||
* @param {Ucanto.Signer} [options.signer] | ||
*/ | ||
|
@@ -16,8 +57,12 @@ export function createProxyHandler(options) { | |
* @returns {Promise<Ucanto.Result<any, { error: true }>>} | ||
*/ | ||
return async function handleInvocation(invocationIn, context) { | ||
const { connections, signer } = options | ||
const { audience, capabilities } = invocationIn | ||
const { | ||
connections, | ||
signer, | ||
catchInvocationError = defaultCatchInvocationError, | ||
} = options | ||
const { audience, capabilities, expiration, notBefore } = invocationIn | ||
const connection = connections[audience.did()] ?? connections.default | ||
// eslint-disable-next-line unicorn/prefer-logical-operator-over-ternary, no-unneeded-ternary | ||
const proxyInvocationIssuer = signer | ||
|
@@ -28,18 +73,26 @@ export function createProxyHandler(options) { | |
: // this works, but involves lying about the issuer type (it wants a Signer but context.id is only a Verifier) | ||
// @todo obviate this type override via https://github.com/web3-storage/ucanto/issues/195 | ||
/** @type {Ucanto.Signer} */ (context.id) | ||
|
||
const [result] = await Client.execute( | ||
[ | ||
Client.invoke({ | ||
issuer: proxyInvocationIssuer, | ||
capability: capabilities[0], | ||
audience, | ||
proofs: [invocationIn], | ||
}), | ||
], | ||
/** @type {Client.ConnectionView<any>} */ (connection) | ||
) | ||
return result | ||
const proxyInvocation = Client.invoke({ | ||
issuer: proxyInvocationIssuer, | ||
capability: capabilities[0], | ||
audience, | ||
proofs: [invocationIn], | ||
Comment on lines
+77
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should add the other props from invocationIn like expiration, etc There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. github is forcing me to add another comments because i approved instead of requesting changes lol There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
You are correct that expiration in the In other words I think it is a good idea to include There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also prompted me to look at take another look at the spec and it seems to state
Which is to say that UCANs that not copying those time bounds would render a UCAN invalid, that said I'm not sure if this is how ucanto does it, but I'll check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah I didn't realize it would default. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created storacha/ucanto#198 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I read it differently... in this case of proxying, the incoming
The time bounds of the proof
The proof would not expire before the 'outer UCAN'. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it would be bad if |
||
expiration, | ||
notBefore, | ||
}) | ||
try { | ||
const [result] = await Client.execute( | ||
[proxyInvocation], | ||
/** @type {Client.ConnectionView<any>} */ (connection) | ||
) | ||
return result | ||
} catch (error) { | ||
if (catchInvocationError) { | ||
const caughtResult = await catchInvocationError(error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we need this to be a promise ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, but resolving a returned promise is intentional so the process of building a result from the error can be async. |
||
return caughtResult | ||
} | ||
throw 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.
we should probably send
error
to sentry here