| Status: | Proposed |
|---|---|
| Date: | 2026-03-31 |
| Deciders: | API Working Group |
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.
- Treat
GETas strictly read-only with respect to domain state: aGEThandler must not create, update, or delete records that are part of the transactional domain model (e.g. enrollments, grades, user profile fields). - Move domain-state-mutating side-effects out of
GEThandlers:- 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
GEThandler 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.
- Create explicit write endpoints (
- Add regression tests to ensure
GEThandlers do not modify domain state. - Document exceptions (if any) and provide migration notes for clients.
- GET used with side-effects: Various views use
@require_GETwhile triggering writes (e.g. tracking, first-access, or logging). Discussion views (lms/djangoapps/discussion/views.py) use@require_GETfor thread/topic listing; any implicit tracking on read should be moved to separate endpoints or async events. - Event emission on read:
common/djangoapps/studentand 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.
Anti-pattern (GET that writes):
@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
@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)- 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.
- 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.
- “Non-Idempotent GET Requests” recommendation in the Open edX REST API standardization notes.