Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/decisions/0030-ensure-get-requests-are-idempotent.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
===============================
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we remove this line to maintain consistency across similar ADRs?

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.
Loading