diff --git a/docs/decisions/0030-ensure-get-requests-are-idempotent.rst b/docs/decisions/0030-ensure-get-requests-are-idempotent.rst new file mode 100644 index 000000000000..366a94a97788 --- /dev/null +++ b/docs/decisions/0030-ensure-get-requests-are-idempotent.rst @@ -0,0 +1,92 @@ +=============================== +Ensure GET is Idempotent +=============================== + +:Status: Proposed +:Date: 2026-03-31 +:Deciders: API Working Group + +Context +======= + +Some Open edX endpoints use ``GET`` requests that have side-effects (e.g., writing tracking logs, +recording first access events). This violates REST safety/idempotency expectations and can break +caching/proxy behavior and automated clients/agents. + +Decision +======== + +1. Treat ``GET`` as strictly read-only for all REST APIs. +2. Move side-effect behavior out of ``GET`` handlers: + + * Create explicit write endpoints (``POST``, ``PUT``, ``PATCH``) for state changes. + * If telemetry must exist, decouple it using async event pipelines (emit events without + mutating domain state) and ensure API responses are not dependent on state mutation. + +3. Add regression tests to ensure ``GET`` handlers do not modify domain state. +4. Document exceptions (if any) and provide migration notes for clients. + +Relevance in edx-platform +========================= + +* **GET used with side-effects**: Various views use ``@require_GET`` while + triggering writes (e.g. tracking, first-access, or logging). Discussion views + (``lms/djangoapps/discussion/views.py``) use ``@require_GET`` for thread/topic + listing; any implicit tracking on read should be moved to separate endpoints or + async events. +* **Event emission on read**: ``common/djangoapps/student`` and courseware code + sometimes emit events (e.g. ``tracker.emit``, streak updates) in code paths + triggered by GET; these should be decoupled so GET handlers do not mutate + domain state. + +Code example +============ + +**Anti-pattern (GET that writes):** + +.. code-block:: python + + @require_GET + def get_progress(request, course_id): + # BAD: recording "first access" or analytics on every GET + record_first_access(request.user, course_id) + return JsonResponse(compute_progress(...)) + +**Preferred: read-only GET + optional separate track endpoint** + +.. code-block:: python + + @require_GET + def get_progress(request, course_id): + return Response(ProgressSerializer(compute_progress(...)).data) + + @require_POST + def track_progress_view(request, course_id): + # Or emit via async pipeline; response does not depend on write + emit_progress_viewed_event(request.user, course_id) + return Response(status=204) + +Consequences +============ + +* Pros + + * REST-compliant behavior; safer automated consumption (AI agents, integrations). + * Predictable caching/proxy semantics. + +* Cons / Costs + + * Requires refactoring legacy courseware/analytics endpoints that currently log on read. + * Potential behavior changes for internal analytics that relied on implicit GET-triggered writes. + +Implementation Notes +==================== + +* Inventory endpoints with GET side-effects. +* For each, define a read-only GET representation and a separate write/track endpoint (or async + event emission) if needed. + +References +========== + +* “Non-Idempotent GET Requests” recommendation in the Open edX REST API standardization notes.