From 995c627fe6345ce2a451589460cd67c4d4cb8257 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven Date: Wed, 16 Jul 2025 17:22:38 +0200 Subject: [PATCH] Add API catch-all endpoint for logging enabled using a setting. Fix #153 --- bugsink/app_settings.py | 1 + bugsink/settings/development.py | 1 + bugsink/urls.py | 4 +++- files/views.py | 41 +++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/bugsink/app_settings.py b/bugsink/app_settings.py index d6e217f..2cafd9e 100644 --- a/bugsink/app_settings.py +++ b/bugsink/app_settings.py @@ -44,6 +44,7 @@ DEFAULTS = { "DIGEST_IMMEDIATELY": True, "VALIDATE_ON_DIGEST": "none", # other legal values are "warn" and "strict" "KEEP_ENVELOPES": 0, # set to a number to store that many; 0 means "store none". This is for debugging. + "API_LOG_UNIMPLEMENTED_CALLS": False, # if True, log unimplemented API calls; see #153 # MAX* below mirror the (current) values for the Sentry Relay "MAX_EVENT_SIZE": _MEBIBYTE, diff --git a/bugsink/settings/development.py b/bugsink/settings/development.py index cc0db7a..72114a9 100644 --- a/bugsink/settings/development.py +++ b/bugsink/settings/development.py @@ -120,6 +120,7 @@ BUGSINK = { "VALIDATE_ON_DIGEST": "warn", # "KEEP_ENVELOPES": 10, + "API_LOG_UNIMPLEMENTED_CALLS": True, # set MAX_EVENTS* very high to be able to do serious performance testing (which I do often in my dev environment) "MAX_EVENTS_PER_PROJECT_PER_5_MINUTES": 1_000_000, diff --git a/bugsink/urls.py b/bugsink/urls.py index 44804f4..3fa95dc 100644 --- a/bugsink/urls.py +++ b/bugsink/urls.py @@ -11,7 +11,7 @@ from teams.views import debug_email as debug_teams_email from bugsink.app_settings import get_settings from users.views import signup, confirm_email, resend_confirmation, request_reset_password, reset_password, preferences from ingest.views import download_envelope -from files.views import chunk_upload, artifact_bundle_assemble +from files.views import chunk_upload, artifact_bundle_assemble, api_catch_all from bugsink.decorators import login_exempt from .views import home, trigger_error, favicon, settings_view, silence_email_system_warning, counts, health_check_ready @@ -52,6 +52,8 @@ urlpatterns = [ path('api/', include('ingest.urls')), + path('api/', api_catch_all, name='api_catch_all'), + # not in /api/ because it's not part of the ingest API, but still part of the ingest app path('ingest/envelope//', download_envelope, name='download_envelope'), diff --git a/files/views.py b/files/views.py index 7372e11..0508d71 100644 --- a/files/views.py +++ b/files/views.py @@ -2,10 +2,12 @@ import json from hashlib import sha1 from gzip import GzipFile from io import BytesIO +import logging from django.http import JsonResponse, HttpResponse from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import user_passes_test +from django.http import Http404 from sentry.assemble import ChunkFileState @@ -16,6 +18,8 @@ from bsmain.models import AuthToken from .models import Chunk, File from .tasks import assemble_artifact_bundle +logger = logging.getLogger("bugsink.api") + _KIBIBYTE = 1024 _MEBIBYTE = 1024 * _KIBIBYTE @@ -200,3 +204,40 @@ def download_file(request, checksum): response = HttpResponse(file.data, content_type="application/octet-stream") response["Content-Disposition"] = f"attachment; filename={file.filename}" return response + + +@csrf_exempt +def api_catch_all(request, subpath): + if not get_settings().API_LOG_UNIMPLEMENTED_CALLS: + raise Http404("Unimplemented API endpoint: /api/" + subpath) + + lines = [ + "Unimplemented API usage:", + f" Path: /api/{subpath}", + f" Method: {request.method}", + ] + + if request.GET: + lines.append(f" GET: {request.GET.dict()}") + + if request.POST: + lines.append(f" POST: {request.POST.dict()}") + + body = request.body + if body: + try: + decoded = body.decode("utf-8", errors="replace").strip() + lines.append(" Body:") + lines.append(f" {decoded[:500]}") + try: + parsed = json.loads(decoded) + pretty = json.dumps(parsed, indent=2)[:10_000] + lines.append(" JSON body:") + lines.extend(f" {line}" for line in pretty.splitlines()) + except json.JSONDecodeError: + pass + except Exception as e: + lines.append(f" Body: ") + + logger.info("\n".join(lines)) + raise Http404("Unimplemented API endpoint: /api/" + subpath)