Skip to content

Latest commit

 

History

History
95 lines (69 loc) · 3.34 KB

File metadata and controls

95 lines (69 loc) · 3.34 KB

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):

@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)

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.