-
Notifications
You must be signed in to change notification settings - Fork 31
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
webhooks: allow to update webhook status & response #1185
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Codecov Report
@@ Coverage Diff @@
## master #1185 +/- ##
===================================================
- Coverage 50.70655% 50.70323% -0.00333%
===================================================
Files 66 66
Lines 4246 4266 +20
Branches 748 755 +7
===================================================
+ Hits 2153 2163 +10
- Misses 1836 1840 +4
- Partials 257 263 +6
Continue to review full report at Codecov.
|
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.
Neat!!
async (req, res) => { | ||
const { id } = req.params; | ||
const webhook = await db.webhook.get(id); | ||
if (!webhook) { |
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.
Gotta check if the webhook is deleted as well
if (!webhook) { | |
if (!webhook || webhook.deleted) { |
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.
Actually, maybe this is one of the few APIs we don't want to check that?
const doc = req.body.status; | ||
const webhookResponse = req.body.response; |
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.
nit: You can grab all the fields with the proper typings! like this:
const doc = req.body.status; | |
const webhookResponse = req.body.response; | |
const { status, response} = req.body as WebhookStatusPayload; |
You could still rename the destructured variables with sth like this const { status: doc, response: webhookResponse}
, but I think the original names of the fields would be the clearest here anyway. doc
kinda hides away what it is, would only use that if there's only one "payload" being handled in the function and doc
is that.
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.
Hmmmmm, actually, thinking further about this, I think the clients shouldn't specify the status
of the webhook. That because the status
field is actually computed from the response:
https://github.com/livepeer/livepeer-com/blob/0f808b4b87eb72942eb098e8c3e4f13ea7699a90/packages/api/src/webhooks/cannon.ts#L456
So we would end up either missing the lastFailure
field which is the most important for debuggability (and that's where the dashboards shows errors from), or the client would have to specify some redundant stuff in their request, with the response (which might be big 📈) going twice. So I am thinking we could do:
- Receive only the
response
object in the request payload - Derive the
status
from it instead, likely reusing some of the same object incannon.ts
(or if you copy:
const triggerTime = response.createdAt ?? Date.now()
let status: DBWebhook["status"] = { lastTriggeredAt: triggerTime };
if (response.statusCode >= 300 || !response.statusCode) {
status = {
...status,
lastFailure: {
timestamp: triggerTime,
statusCode: response.statusCode,
response: response.response.body,
},
};
}
await this.db.webhook.updateStatus(response.webhookId, status);
)
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.
I guess this makes sense, but what about the cases where we don't get any response from the webhook? For example, when there is a connection reset or a failed DNS resolve, we actually don't have a status code and a response. Unless if the status code is missing then we consider it a failure and updated LastFailure
accordingly?
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.
Ok I missed that it's actually handled! Sounds good!
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.
I think we also have an errorMessage
field in the webhook status to account for those non-HTTP failures. We could probably receive it from the caller as well, as a separate field apart from the response (just like on the status as well afaik). WDYT?
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.
Cool, makes sense! I am keeping also the errorMessage
as a separate field. response.statusCode
is a required field tho in the payload, so I either set up the statusCode
as 0 when the response fails or I make it optional, wdyt?
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.
You could have a request like this
{
errorMessage: foo,
response: { status: 123, ... }
}
In which status(code) is a required field of response, but response is not a required field of that payload. So you can omit the whole response if you got some different error, but if you do send a response it must have a status code. WDYT?
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.
FTR: We decided to simplify the first version and just comment out the webhook_response
table update. Will do a follow-up to start saving the responses soon.
webhookResponse.id = uuid(); | ||
let w: WithID<WebhookResponse> = webhookResponse; | ||
await db.webhookResponse.create(w); |
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.
typescript rescue
webhookResponse.id = uuid(); | |
let w: WithID<WebhookResponse> = webhookResponse; | |
await db.webhookResponse.create(w); | |
await db.webhookResponse.create({ | |
...webhookResponse, | |
id: uuid() | |
}); |
and if it still complains on the typing, can change last line to
} as WithID<WebhookResponse>);
const payload = { | ||
status: { | ||
lastTriggeredAt: Date.now(), | ||
lastFailure: { timestamp: Date.now() }, | ||
}, | ||
}; |
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.
You should try sending a response here as well to catch 1 issue that you likely haven't noticed 👀
The webhook-response
object has a required eventId
because that's how it works for all webhook calls right now. I think we should make that field optional instead, just so the client doesn't have to generate some random ID just to fill that field.
return res.status(422).json({ errors: ["missing status in payload"] }); | ||
} | ||
|
||
await db.webhook.update(req.params.id, { |
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.
There is a dedicated updateStatus
function which should be used instead. Try taking a look on the cannon.ts
code that does these updates to make sure you are doing the same thing here (or if there's anything you could reuse, like exposing some of the logic there as helpers):
https://github.com/livepeer/livepeer-com/blob/0f808b4b87eb72942eb098e8c3e4f13ea7699a90/packages/api/src/webhooks/cannon.ts#L446-L503
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.
LGTM again, but I think a few bugs would be fixed and it'd be a lot cleaner if the response
field was not required in the payload of the new API. It is not always present and we shouldn't force it to be. Would naturally fix the bug of webhook responses by written even when there is no response data.
if (!webhook) { | ||
return res.status(404).json({ errors: ["not found"] }); | ||
if (!webhook || webhook.deleted) { | ||
return res.status(404).json({ errors: ["webhook not found or deleted"] }); |
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.
For the user, it's just a not found!
return res.status(404).json({ errors: ["webhook not found or deleted"] }); | |
return res.status(404).json({ errors: ["webhook not found"] }); |
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.
I added deleted because it's never a user call but an admin one! But yeah, could remove it!
if (!response || !response.response) { | ||
return res.status(422).json({ errors: ["missing response in payload"] }); | ||
} | ||
if (!response.response.body && response.response.body !== "") { | ||
return res | ||
.status(400) | ||
.json({ errors: ["missing body in payload response"] }); | ||
} |
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.
The response should be an optional field since the webhook call could have gotten an error (errorMessage
) but not an actual HTTP response.
Apart from that, even if we wanted to have a required field here, this logic should be handled on the schema level, by making the field required with required: [response]
. There are some more "semantic" checks that can't be declared too easily in a JSON schema, but otherwise we should not be doing "json schema" checks in our code as much as possible.
if ( | ||
response.statusCode >= 300 || | ||
!response.statusCode || | ||
response.statusCode === 0 | ||
) { |
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.
Response field should be optional since not always there will be a response, and statusCode
should be obligatory (checked by JSON schema) and so we don't need to check if it's present here:
if ( | |
response.statusCode >= 300 || | |
!response.statusCode || | |
response.statusCode === 0 | |
) { | |
if ( | |
errorMessage || | |
response?.statusCode >= 300 | |
) { |
statusCode: response.statusCode, | ||
response: response.response.body, |
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.
statusCode: response.statusCode, | |
response: response.response.body, | |
statusCode: response?.statusCode, | |
response: response?.response.body, |
await db.webhookResponse.create({ | ||
id: uuid(), | ||
webhookId: webhook.id, | ||
createdAt: Date.now(), | ||
statusCode: response.statusCode, | ||
response: { | ||
body: response.response.body, | ||
status: response.statusCode, | ||
}, | ||
}); |
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.
Need to surround this with if (response)
. It will not always be present and we shouldn't be writing empty/zeroed entries on the webhook_response
if we're not saving the error message in there as well (we could save the error message 👀)
packages/api/src/schema/schema.yaml
Outdated
additionalProperties: false | ||
properties: |
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.
Right! On this object you would declare
additionalProperties: false | |
properties: | |
additionalProperties: false | |
required: [response] | |
properties: |
To make the response field obligatory and remove the need to check manually on your API handler code. BUT I don't think it should be required anyway, just explaining here what you could do on another case like this (or if you prefer to keep it as required)
@@ -273,7 +273,6 @@ components: | |||
table: webhook_response | |||
required: | |||
- webhookId | |||
- eventId | |||
- statusCode |
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.
Turns out the statusCode
field is already required. No need to have those checks in the code :)
body: response.response.body, | ||
status: response.statusCode, |
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.
There is also a lot more stuff in the response object. Please save at least the response.headers
here as well but do go through the schema to figure out anything else we want to record. Ideally we should at least keep it consistent with the webhook cannon code, which right now it still isnt'.
$ref: "#/components/schemas/webhook/properties/status" | ||
errorMessage: | ||
type: string | ||
description: Error message if the webhook failed to process the event | ||
response: | ||
$ref: "#/components/schemas/webhook-response" |
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.
Hmm... Maybe here we actually want to send a "#/components/schemas/webhook-response/properties/response"
instead? seems like this objet has a lot of other fields that we don't need while we only really use the response.response
field anyway (there is a status code in there too). Would simplify the client and the server-side code. This is the most optional change.
As per async discussion, for faster debugging of token gating the |
@@ -29,7 +29,7 @@ export default class WebhookTable extends Table<DBWebhook> { | |||
const query = [sql`data->>'userId' = ${userId}`]; | |||
if (streamId) { | |||
query.push( | |||
sql`data->>'streamId' = ${streamId} OR data->>'streamId' IS NULL` | |||
sql`(data->>'streamId' = ${streamId} OR data->>'streamId' IS NULL)` |
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.
Nice! At some point we should just do that inside that find()
function that concats everything with an AND
. Shouldn't be the consumer's responsibility to care about this.
What does this pull request do? Explain your changes. (required)
Not in the weekly planning, but needed while debugging token gating:
All the webhooks success & failures are currently updated by Studio, apart of the webhook
playback.user.new
(token gating) which is handled on mapic. This new api allows admins (& mapic) to update the status of a webhook and store the responses, allowing us to:This needs some changes on mapic to store this information.
Specific updates (required)
How did you test each of these updates (required)
yarn test
Does this pull request close any open issues?
FIxes #1171
Screenshots (optional):
Checklist: