Skip to content

✨(backend) add OpenShift-compatible recording download endpoint - #1622

Open
mbenhammouda wants to merge 1 commit into
mainfrom
feat/recording-download-openshift
Open

✨(backend) add OpenShift-compatible recording download endpoint#1622
mbenhammouda wants to merge 1 commit into
mainfrom
feat/recording-download-openshift

Conversation

@mbenhammouda

Copy link
Copy Markdown
Collaborator

Implement a Django-based download endpoint that streams recording files directly from S3 and removes the dependency on NGINX Ingress authentication annotations required by the existing download flow.

Purpose

The current recording download flow relies on NGINX Ingress authentication annotations (auth-url and auth-response-headers) to authorize access to recordings stored in S3.

This approach works with NGINX Ingress but is not supported by OpenShift, making the recording download feature unavailable on OpenShift deployments.

Proposal

Introduce a dedicated Django endpoint to handle recording downloads without relying on ingress-level authentication annotations.

The endpoint:

  • Validates the recording identifier and file extension
  • Enforces the same authorization rules as the existing media_auth endpoint
  • Ensures the recording is in a saved state before allowing downloads
  • Retrieves the file directly from S3 using the application's credentials
  • Streams the content to the client using StreamingHttpResponse
  • Returns download-related HTTP headers (Content-Disposition, Content-Type, Content-Length) to trigger a native browser download.

Benefits

  • Enable recording downloads on OpenShift deployments.
  • Remove dependency on NGINX-specific ingress annotations.
  • Preserve existing security and permission checks.
  • Stream files efficiently without loading them entirely into memory.
  • Provide a platform-independent implementation.

@mbenhammouda
mbenhammouda force-pushed the feat/recording-download-openshift branch 2 times, most recently from fc01b5c to 1905897 Compare August 20, 2026 13:38
@mbenhammouda
mbenhammouda marked this pull request as ready for review August 20, 2026 14:11
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add OpenShift-compatible recording download endpoint (stream from S3 via Django)

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add Django endpoint to download recordings without NGINX ingress auth_request annotations.
• Enforce existing recording permissions and saved-state checks before streaming bytes.
• Add test coverage for download authorization and response headers.
Diagram

graph TD
  A["Browser client"] --> B["/media/recordings/<id>.<ext>"] --> C["RecordingViewSet.media_download"] --> D{"Validate + authorize"} -->|"ok"| E[("S3/MinIO via default_storage")] --> F["StreamingHttpResponse"] --> G["Native download"]
  D -->|"reject"| H["4xx response"]

  subgraph Legend
    direction LR
    _cli["Client"] ~~~ _svc["Django handler"] ~~~ _dec{"Decision"} ~~~ _obj[("Object storage")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Return a pre-signed S3 URL (redirect)
  • ➕ Avoids routing large downloads through Django workers
  • ➕ Lets clients download directly from object storage (better throughput/cost)
  • ➖ Requires storage endpoint reachable from clients (networking/CORS/DNS)
  • ➖ More moving parts to keep consistent with current permission model and expiry semantics
2. Keep ingress-level auth_request (NGINX annotations)
  • ➕ No backend bandwidth usage for downloads
  • ➕ Simple at application layer
  • ➖ Not supported on OpenShift; blocks feature availability
  • ➖ Ingress-specific coupling makes portability harder

Recommendation: The streaming Django proxy is a reasonable, platform-independent fix for OpenShift where ingress auth_request isn’t available and object storage may not be directly exposed. If download volume becomes significant, consider evolving toward short-lived pre-signed URLs (when networking permits) to reduce backend bandwidth and worker utilization.

Files changed (4) +126 / -1

Enhancement (2) +70 / -1
viewsets.pyAdd media_download action streaming recordings from S3/MinIO +63/-1

Add media_download action streaming recordings from S3/MinIO

• Introduces RecordingViewSet.media_download, a GET endpoint that validates UUID/extension, mirrors media_auth authorization (retrieve ability + saved state), fetches the object from S3 via default_storage, and streams it via StreamingHttpResponse with download headers.

src/backend/core/api/viewsets.py

urls.pyRoute /media/recordings/<uuid>.<ext> to media_download +7/-0

Route /media/recordings/<uuid>.<ext> to media_download

• Registers a new URL pattern mapping the media recordings path to RecordingViewSet.media_download for direct, platform-independent downloads.

src/backend/core/urls.py

Tests (1) +52 / -0
test_api_recordings_retrieve.pyAdd tests for direct media download authorization and streaming +52/-0

Add tests for direct media download authorization and streaming

• Adds a helper for the media download URL, asserts 401/403 behavior for anonymous/unauthorized cases, and adds a parameterized test for owner/administrator downloads using a mocked S3 get_object response and verifying headers and streamed content.

src/backend/core/tests/recording/test_api_recordings_retrieve.py

Documentation (1) +4 / -0
CHANGELOG.mdDocument new OpenShift-compatible recording download endpoint +4/-0

Document new OpenShift-compatible recording download endpoint

• Adds an entry under Unreleased/Added noting the new backend download endpoint.

CHANGELOG.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Unhandled S3 get_object errors 🐞 Bug ☼ Reliability
Description
RecordingViewSet.media_download calls s3_client.get_object without handling storage/client errors,
so missing objects or transient S3/MinIO failures will surface as 500 responses instead of a
controlled 404/503. This can break downloads in normal operational scenarios and may expose internal
exception details via the API error handler.
Code

src/backend/core/api/viewsets.py[R1265-1269]

+        s3_client = default_storage.connection.meta.client
+        s3_object = s3_client.get_object(
+            Bucket=default_storage.bucket_name,
+            Key=recording.key,
+        )
Evidence
The new endpoint directly calls default_storage.connection.meta.client.get_object(...) with no
surrounding try/except; therefore any raised exception will propagate and become a 500 handled by
DRF's exception handler instead of a controlled API response.

src/backend/core/api/viewsets.py[1223-1283]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`media_download` performs a direct `get_object` call to S3/MinIO without any exception handling. Any `ClientError` (eg, NoSuchKey), connectivity issues, or misconfiguration will bubble up as a 500.

### Issue Context
This endpoint is intended to replace ingress-level behavior; it should fail in a predictable way (eg 404 if object missing, 503 if backend unavailable) rather than crashing the request.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1265-1283]

### Suggested fix
- Wrap `get_object` in `try/except`.
- Map "object not found"-type errors to `drf_exceptions.NotFound`.
- Map transient backend issues to `drf_exceptions.APIException` (or a dedicated 503 exception if you have one in the project).
- Add a test that simulates `get_object` raising and asserts the expected HTTP status code.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Streaming body never closed 🐞 Bug ☼ Reliability
Description
media_download streams s3_object["Body"].iter_chunks(...) but never closes the underlying
body/connection, which can leak S3/MinIO HTTP connections (especially on client disconnects) and
eventually exhaust worker or backend resources. This is a long-lived endpoint for large downloads,
so resource cleanup needs to be explicit.
Code

src/backend/core/api/viewsets.py[R1275-1278]

+        streaming_response = StreamingHttpResponse(
+            s3_object["Body"].iter_chunks(chunk_size=65536),
+            content_type=content_type,
+        )
Evidence
The implementation uses the S3 response body's iterator as streaming_content and returns
immediately; there is no finally/cleanup path that closes s3_object["Body"].

src/backend/core/api/viewsets.py[1265-1283]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`StreamingHttpResponse` is fed an iterator created from `s3_object["Body"].iter_chunks(...)`, but the code never ensures `s3_object["Body"]` is closed.

### Issue Context
This endpoint is a streaming proxy for potentially large media files. If the client disconnects early or an exception occurs mid-stream, not closing the upstream body can leave connections open.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1271-1283]

### Suggested fix
- Create a small generator wrapper:
 - `body = s3_object["Body"]`
 - `def stream():
       try:
           yield from body.iter_chunks(chunk_size=65536)
       finally:
           body.close()`
 - Pass `stream()` to `StreamingHttpResponse`.
- Add/extend a unit test to assert `close()` is called on the mocked body when the iterator is exhausted (and ideally when aborted).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Zero Content-Length dropped 🐞 Bug ≡ Correctness
Description
media_download only sets the Content-Length header when content_length is truthy; for valid
zero-byte objects (ContentLength == 0) the header will be omitted. This is a small correctness issue
that can cause inconsistent client behavior vs always emitting the known length.
Code

src/backend/core/api/viewsets.py[R1280-1282]

+        if content_length:
+            streaming_response["Content-Length"] = content_length
+
Evidence
The new code reads ContentLength then conditionally sets the header only when the value is truthy,
which excludes 0.

src/backend/core/api/viewsets.py[1271-1282]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The code checks `if content_length:` before setting the header, which skips the header when the length is 0.

### Issue Context
S3 returns `ContentLength` as an integer that can legitimately be 0.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1271-1282]

### Suggested fix
Change the condition to `if content_length is not None:` so 0 is preserved.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/backend/core/api/viewsets.py Outdated
Comment on lines +1265 to +1269
s3_client = default_storage.connection.meta.client
s3_object = s3_client.get_object(
Bucket=default_storage.bucket_name,
Key=recording.key,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Unhandled s3 get_object errors 🐞 Bug ☼ Reliability

RecordingViewSet.media_download calls s3_client.get_object without handling storage/client errors,
so missing objects or transient S3/MinIO failures will surface as 500 responses instead of a
controlled 404/503. This can break downloads in normal operational scenarios and may expose internal
exception details via the API error handler.
Agent Prompt
### Issue description
`media_download` performs a direct `get_object` call to S3/MinIO without any exception handling. Any `ClientError` (eg, NoSuchKey), connectivity issues, or misconfiguration will bubble up as a 500.

### Issue Context
This endpoint is intended to replace ingress-level behavior; it should fail in a predictable way (eg 404 if object missing, 503 if backend unavailable) rather than crashing the request.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1265-1283]

### Suggested fix
- Wrap `get_object` in `try/except`.
- Map "object not found"-type errors to `drf_exceptions.NotFound`.
- Map transient backend issues to `drf_exceptions.APIException` (or a dedicated 503 exception if you have one in the project).
- Add a test that simulates `get_object` raising and asserts the expected HTTP status code.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1275 to +1278
streaming_response = StreamingHttpResponse(
s3_object["Body"].iter_chunks(chunk_size=65536),
content_type=content_type,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Streaming body never closed 🐞 Bug ☼ Reliability

media_download streams s3_object["Body"].iter_chunks(...) but never closes the underlying
body/connection, which can leak S3/MinIO HTTP connections (especially on client disconnects) and
eventually exhaust worker or backend resources. This is a long-lived endpoint for large downloads,
so resource cleanup needs to be explicit.
Agent Prompt
### Issue description
`StreamingHttpResponse` is fed an iterator created from `s3_object["Body"].iter_chunks(...)`, but the code never ensures `s3_object["Body"]` is closed.

### Issue Context
This endpoint is a streaming proxy for potentially large media files. If the client disconnects early or an exception occurs mid-stream, not closing the upstream body can leave connections open.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1271-1283]

### Suggested fix
- Create a small generator wrapper:
  - `body = s3_object["Body"]`
  - `def stream():
        try:
            yield from body.iter_chunks(chunk_size=65536)
        finally:
            body.close()`
  - Pass `stream()` to `StreamingHttpResponse`.
- Add/extend a unit test to assert `close()` is called on the mocked body when the iterator is exhausted (and ideally when aborted).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1280 to +1282
if content_length:
streaming_response["Content-Length"] = content_length

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Zero content-length dropped 🐞 Bug ≡ Correctness

media_download only sets the Content-Length header when content_length is truthy; for valid
zero-byte objects (ContentLength == 0) the header will be omitted. This is a small correctness issue
that can cause inconsistent client behavior vs always emitting the known length.
Agent Prompt
### Issue description
The code checks `if content_length:` before setting the header, which skips the header when the length is 0.

### Issue Context
S3 returns `ContentLength` as an integer that can legitimately be 0.

### Fix Focus Areas
- src/backend/core/api/viewsets.py[1271-1282]

### Suggested fix
Change the condition to `if content_length is not None:` so 0 is preserved.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@mbenhammouda
mbenhammouda force-pushed the feat/recording-download-openshift branch 4 times, most recently from bb40cf4 to 2721bcd Compare August 20, 2026 14:51
Implement a Django-based download endpoint that streams recording files
@mbenhammouda
mbenhammouda force-pushed the feat/recording-download-openshift branch from 2721bcd to 7ed6091 Compare August 20, 2026 14:54
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant