Skip to content

Commit 2be7a51

Browse files
Fix MultipleObjectsReturned error when multiple integrations exist (#12581)
Fixes a error that occurs when a project has multiple integrations with the same . ## Changes Modified method in to: - Check the count of matching integrations - Raise a clear (400 Bad Request) when multiple integrations are found - Provide an actionable error message directing users to use the webhook URL specific to their integration ## References * Sentry issue: https://read-the-docs.sentry.io/issues/4951512277/ * Related issue: #11037 --- *Generated by Copilot* --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 227df90 commit 2be7a51

File tree

2 files changed

+56
-8
lines changed

2 files changed

+56
-8
lines changed

readthedocs/api/v2/views/integrations.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from textwrap import dedent
88

99
import structlog
10+
from django.http import Http404
1011
from django.shortcuts import get_object_or_404
1112
from django.utils.crypto import constant_time_compare
1213
from rest_framework import permissions
@@ -152,12 +153,20 @@ def finalize_response(self, req, *args, **kwargs):
152153
"""If the project was set on POST, store an HTTP exchange."""
153154
resp = super().finalize_response(req, *args, **kwargs)
154155
if hasattr(self, "project") and self.project:
155-
HttpExchange.objects.from_exchange(
156-
req,
157-
resp,
158-
related_object=self.get_integration(),
159-
payload=self.data,
160-
)
156+
try:
157+
integration = self.get_integration()
158+
except (Http404, ParseError):
159+
# If we can't get a single integration (either none or multiple exist),
160+
# we can't store the HTTP exchange
161+
integration = None
162+
163+
if integration:
164+
HttpExchange.objects.from_exchange(
165+
req,
166+
resp,
167+
related_object=integration,
168+
payload=self.data,
169+
)
161170
return resp
162171

163172
def get_data(self):
@@ -203,11 +212,21 @@ def get_integration(self):
203212
# in `WebhookView`
204213
if self.integration is not None:
205214
return self.integration
206-
self.integration = get_object_or_404(
207-
Integration,
215+
216+
integrations = Integration.objects.filter(
208217
project=self.project,
209218
integration_type=self.integration_type,
210219
)
220+
221+
if not integrations.exists():
222+
raise Http404("No Integration matches the given query.")
223+
elif integrations.count() > 1:
224+
raise ParseError(
225+
"Multiple integrations found for this project. "
226+
"Please use the webhook URL with an explicit integration ID."
227+
)
228+
229+
self.integration = integrations.first()
211230
return self.integration
212231

213232
def get_response_push(self, project, versions_info: list[VersionInfo]):

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3502,6 +3502,35 @@ def test_dont_allow_webhooks_without_a_secret(self, trigger_build):
35023502
# and return a 400 if it doesn't.
35033503
self.assertEqual(resp.status_code, 404)
35043504

3505+
def test_multiple_integrations_error(self, trigger_build):
3506+
"""Test that multiple integrations with same type returns a 400 error."""
3507+
client = APIClient()
3508+
3509+
# Create a second GitHub integration for the same project with the same secret
3510+
secret = self.github_integration.secret
3511+
Integration.objects.create(
3512+
project=self.project,
3513+
integration_type=Integration.GITHUB_WEBHOOK,
3514+
secret=secret,
3515+
)
3516+
3517+
# Now there are two integrations, so the webhook should return a 400 error
3518+
payload = {"ref": "refs/heads/master"}
3519+
signature = get_signature(self.github_integration, payload)
3520+
3521+
resp = client.post(
3522+
f"/api/v2/webhook/github/{self.project.slug}/",
3523+
payload,
3524+
format="json",
3525+
headers={
3526+
GITHUB_SIGNATURE_HEADER: signature,
3527+
},
3528+
)
3529+
3530+
# Should return 400 Bad Request
3531+
self.assertEqual(resp.status_code, 400)
3532+
self.assertIn("Multiple integrations found", resp.data["detail"])
3533+
35053534

35063535
@override_settings(PUBLIC_DOMAIN="readthedocs.io")
35073536
class APIVersionTests(TestCase):

0 commit comments

Comments
 (0)