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
95 changes: 95 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,95 @@
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 with respect to **domain state**: a ``GET`` handler
must not create, update, or delete records that are part of the transactional domain model
(e.g. enrollments, grades, user profile fields).
2. Move domain-state-mutating side-effects out of ``GET`` handlers:

* Create explicit write endpoints (``POST``, ``PUT``, ``PATCH``) for state changes.
* Telemetry and analytics writes to a **separate analytics store** (e.g. event tracking,
segment events, read-count increments) are acceptable inside a ``GET`` handler provided
the response content does not depend on them. These writes do not need to be moved to
async pipelines unless there is a specific performance or reliability reason to do so.

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