webhooks: Use 200 status code for unknown events.

Because the third party might not be expecting a 400 from our
webhooks, we now instead use 200 status code for unknown events,
while sending back the error to Sentry. Because it is no longer an error
response, the response type should now be "success".

Fixes #24721.
This commit is contained in:
Zixuan James Li 2023-04-04 01:09:30 -04:00 committed by Tim Abbott
parent 2e4f7f6336
commit 84723654c8
9 changed files with 52 additions and 10 deletions

View File

@ -33,6 +33,7 @@ from django.utils.timezone import now as timezone_now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django_otp import user_has_device from django_otp import user_has_device
from sentry_sdk import capture_exception
from two_factor.utils import default_device from two_factor.utils import default_device
from typing_extensions import Concatenate, ParamSpec from typing_extensions import Concatenate, ParamSpec
@ -366,6 +367,8 @@ def webhook_view(
else: else:
if isinstance(err, WebhookError): if isinstance(err, WebhookError):
err.webhook_name = webhook_client_name err.webhook_name = webhook_client_name
if isinstance(err, UnsupportedWebhookEventTypeError):
capture_exception(err)
log_exception_to_webhook_logger(err) log_exception_to_webhook_logger(err)
raise err raise err
@ -775,6 +778,8 @@ def authenticated_rest_api_view(
if isinstance(err, WebhookError): if isinstance(err, WebhookError):
err.webhook_name = webhook_client_name err.webhook_name = webhook_client_name
if isinstance(err, UnsupportedWebhookEventTypeError):
capture_exception(err)
log_exception_to_webhook_logger(err) log_exception_to_webhook_logger(err)
raise err raise err

View File

@ -370,6 +370,7 @@ class UnsupportedWebhookEventTypeError(WebhookError):
""" """
code = ErrorCode.UNSUPPORTED_WEBHOOK_EVENT_TYPE code = ErrorCode.UNSUPPORTED_WEBHOOK_EVENT_TYPE
http_status_code = 200
data_fields = ["webhook_name", "event_type"] data_fields = ["webhook_name", "event_type"]
def __init__(self, event_type: Optional[str]) -> None: def __init__(self, event_type: Optional[str]) -> None:
@ -378,7 +379,9 @@ class UnsupportedWebhookEventTypeError(WebhookError):
@staticmethod @staticmethod
def msg_format() -> str: def msg_format() -> str:
return _("The '{event_type}' event isn't currently supported by the {webhook_name} webhook") return _(
"The '{event_type}' event isn't currently supported by the {webhook_name} webhook; ignoring"
)
class AnomalousWebhookPayloadError(WebhookError): class AnomalousWebhookPayloadError(WebhookError):

View File

@ -117,8 +117,11 @@ def json_response_from_error(exception: JsonableError) -> MutableJsonResponse:
middleware takes care of transforming it into a response by middleware takes care of transforming it into a response by
calling this function. calling this function.
""" """
response_type = "error"
if 200 <= exception.http_status_code < 300:
response_type = "success"
response = json_response( response = json_response(
"error", msg=exception.msg, data=exception.data, status=exception.http_status_code response_type, msg=exception.msg, data=exception.data, status=exception.http_status_code
) )
for header, value in exception.extra_headers.items(): for header, value in exception.extra_headers.items():

View File

@ -406,7 +406,9 @@ class DecoratorTestCase(ZulipTestCase):
request = HostRequestMock() request = HostRequestMock()
request.host = "zulip.testserver" request.host = "zulip.testserver"
request.POST["api_key"] = webhook_bot_api_key request.POST["api_key"] = webhook_bot_api_key
exception_msg = "The 'test_event' event isn't currently supported by the ClientName webhook" exception_msg = (
"The 'test_event' event isn't currently supported by the ClientName webhook; ignoring"
)
with self.assertLogs("zulip.zerver.webhooks.unsupported", level="ERROR") as log: with self.assertLogs("zulip.zerver.webhooks.unsupported", level="ERROR") as log:
with self.assertRaisesRegex(UnsupportedWebhookEventTypeError, exception_msg): with self.assertRaisesRegex(UnsupportedWebhookEventTypeError, exception_msg):
request.body = b"invalidjson" request.body = b"invalidjson"
@ -577,9 +579,7 @@ class DecoratorLoggingTestCase(ZulipTestCase):
with mock.patch( with mock.patch(
"zerver.decorator.webhook_unsupported_events_logger.exception" "zerver.decorator.webhook_unsupported_events_logger.exception"
) as mock_exception: ) as mock_exception:
exception_msg = ( exception_msg = "The 'test_event' event isn't currently supported by the ClientName webhook; ignoring"
"The 'test_event' event isn't currently supported by the ClientName webhook"
)
with self.assertRaisesRegex(UnsupportedWebhookEventTypeError, exception_msg): with self.assertRaisesRegex(UnsupportedWebhookEventTypeError, exception_msg):
my_webhook_raises_exception(request) my_webhook_raises_exception(request)

View File

@ -565,7 +565,7 @@ A temporary team so that I can get some webhook fixtures!
stack_info = m.call_args[1]["stack_info"] stack_info = m.call_args[1]["stack_info"]
self.assertIn( self.assertIn(
"The 'team/edited (changes: bogus_key1/bogus_key2)' event isn't currently supported by the GitHub webhook", "The 'team/edited (changes: bogus_key1/bogus_key2)' event isn't currently supported by the GitHub webhook; ignoring",
msg, msg,
) )
self.assertTrue(stack_info) self.assertTrue(stack_info)

View File

@ -55,7 +55,7 @@ class Helper:
self.include_title = include_title self.include_title = include_title
def log_unsupported(self, event: str) -> None: def log_unsupported(self, event: str) -> None:
summary = f"The '{event}' event isn't currently supported by the GitHub webhook" summary = f"The '{event}' event isn't currently supported by the GitHub webhook; ignoring"
log_unsupported_webhook_event( log_unsupported_webhook_event(
summary=summary, summary=summary,
) )

View File

@ -80,7 +80,8 @@ class PagerDutyHookTests(WebhookTestCase):
for version in range(1, 4): for version in range(1, 4):
payload = self.get_body(f"unsupported_v{version}") payload = self.get_body(f"unsupported_v{version}")
result = self.client_post(self.url, payload, content_type="application/json") result = self.client_post(self.url, payload, content_type="application/json")
self.assert_json_error( self.assert_json_success(result)
self.assert_in_response(
"The 'incident.unsupported' event isn't currently supported by the PagerDuty webhook; ignoring",
result, result,
"The 'incident.unsupported' event isn't currently supported by the PagerDuty webhook",
) )

View File

@ -0,0 +1,27 @@
[{
"event": "unknown",
"check": {
"token": "ngg8",
"url": "https://updown.io",
"alias": "",
"last_status": 200,
"uptime": 99.954,
"down": false,
"down_since": null,
"error": null,
"period": 30,
"apdex_t": 0.25,
"string_match": "",
"enabled": true,
"published": true,
"last_check_at": "2016-02-07T13:16:07Z",
"next_check_at": "2016-02-07T13:16:37Z",
"favicon_url": "https://updown.io/favicon.png"
},
"downtime": {
"error": null,
"started_at": null,
"ended_at": null,
"duration": null
}
}]

View File

@ -52,3 +52,6 @@ class UpdownHookTests(WebhookTestCase):
topic_name=topic_name, topic_name=topic_name,
content=up_content, content=up_content,
) )
def test_unknown_event(self) -> None:
self.check_webhook("unknown_event", expect_noop=True)