Bối cảnh

Bài toán: serve file tĩnh có kiểm soát quyền truy cập — ví dụ artifact HTML do hệ thống AI sinh ra cho từng user, báo cáo riêng tư, file đính kèm nội bộ. File là tĩnh, nhưng không thể đặt vào /static/ công khai vì chỉ chủ sở hữu mới được xem.

Cách làm ngây thơ là để Django đọc file và trả bytes:

Client → Nginx → Django → đọc file → trả bytes → Nginx → Client

Chạy được, nhưng Django worker bị chiếm suốt thời gian truyền file, file lớn thì chiếm luôn RAM, và mỗi download đắt ngang một request động. Điều vô lý ở đây: phần khó (kiểm tra quyền) chỉ mất vài ms, phần nặng (đẩy bytes) lại là việc nginx làm giỏi hơn Django cả trăm lần — mà ta đang bắt Django làm cả hai.

X-Accel-Redirect tách đúng chỗ đó: application server chỉ trả lời câu hỏi "user này có được xem file này không", rồi ủy quyền cho nginx serve file qua một internal redirect. Application server là gì không quan trọng — bài này minh họa bằng Django, nhưng FastAPI, Kotlin/Jakarta hay Node đều chơi được, vì hợp đồng chỉ là một HTTP header.

Client → Nginx → Django (chỉ check auth) → X-Accel-Redirect → Nginx serve file → Client

Cơ chế

Sơ đồ tuần tự X-Accel-Redirect Client gửi request, nginx proxy về Django, Django chỉ kiểm tra quyền rồi trả header X-Accel-Redirect, nginx đọc file từ filesystem qua location internal và stream về client. Client Nginx Django Filesystem GET /artifact/{id} proxy_pass Check session, ownership 200, X-Accel-Redirect Đọc file (location internal) File bytes Stream file về client

Toàn bộ ma thuật nằm ở hai chữ: header X-Accel-Redirect trong response của Django không bao giờ đến client. Nginx bắt lấy nó, hiểu là "hãy xử lý lại request này ở location sau đây", và thực hiện internal redirect ngay trong chính nó.

Cấu hình nginx

# Public endpoint — forward đến Django
location /artifact/ {
    proxy_pass http://127.0.0.1:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
}

# Internal location — chỉ nginx nội bộ mới với tới được
location /internal/artifacts/ {
    internal;
    alias /var/app/artifacts/;
}

Từ khóa internal là chốt an toàn: client request thẳng /internal/artifacts/... sẽ nhận 404 — location này chỉ tồn tại với các internal redirect. Đường dẫn thật của file trên disk cũng ẩn hoàn toàn khỏi client.

Phía Django

from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import get_object_or_404

from .models import Artifact


@login_required
def artifact_view(request, artifact_id):
    artifact = get_object_or_404(Artifact, id=artifact_id)

    if artifact.user_id != request.user.id:
        return HttpResponse(status=403)

    response = HttpResponse(content_type="text/html")
    response["X-Accel-Redirect"] = f"/internal/artifacts/{artifact.file_path}"
    return response

Response của Django rỗng — không một byte file nào đi qua nó. Worker được giải phóng ngay sau khi check quyền xong, còn nginx dùng sendfile stream thẳng từ disk.

Schema tối thiểu đi kèm:

CREATE TABLE artifact (
    id              UUID PRIMARY KEY,
    user_id         INTEGER NOT NULL REFERENCES auth_user(id),
    conversation_id VARCHAR(64),
    file_path       VARCHAR(512) NOT NULL,  -- 2025-04/{user_id}/{artifact_id}.html
    chart_type      VARCHAR(32),
    created_at      TIMESTAMP DEFAULT NOW()
);

Dùng UUID cho artifact_id thay vì sequential ID — URL dạng /artifact/42/ mời gọi người ta thử /artifact/43/; UUID chặn enumeration ngay từ không gian định danh, còn ownership check là lớp chặn thật.

Ứng dụng: artifact do LLM sinh ra

Ngữ cảnh khiến tôi dùng pattern này: hệ thống AI nội bộ nơi LLM gọi MCP tool để sinh biểu đồ ECharts dưới dạng file HTML. Điểm đáng chú ý về kiến trúc: MCP server là một process hoàn toàn độc lập (Node.js), không liên quan gì đến Django — nó không gọi Django, Django cũng không gọi nó. Ba bên chỉ chia sẻ hai thứ: filesystem và bảng artifact trong DB.

Ghi (MCP server — độc lập):
  User hỏi → LLM gọi MCP tool generate_chart
           → MCP render HTML, ghi file /var/app/artifacts/{YYYY-MM}/{user_id}/{artifact_id}.html
           → MCP ghi record vào bảng artifact, trả artifact_id cho LLM
           → LLM trả lời kèm tag <artifact id="..." type="echart"/>

Đọc (Django + nginx — độc lập):
  Client parse tag, render iframe trỏ GET /artifact/{id}
           → Django tra bảng artifact, check quyền → X-Accel-Redirect → nginx serve

Hợp đồng giữa hai phía chỉ là schema bảng artifact và quy ước đường dẫn file_path — MCP server ghi theo format nào, Django tin theo format đó. Không API call chéo, không coupling runtime: MCP server chết không ảnh hưởng việc xem artifact cũ, Django chết không ảnh hưởng việc sinh artifact mới. Việc phân quyền trên filesystem cũng tách bạch theo: process MCP cần quyền ghi vào /var/app/artifacts/, nginx chỉ cần quyền đọc, còn Django không đụng vào file — nó chỉ đọc DB.

Đặc điểm của loại file này khớp hoàn hảo với pattern: sinh một lần, đọc nhiều lần, gắn chặt với một user, và có thể nặng (chart nhiều data point). Django chỉ trả lời đúng một câu hỏi mỗi request; mọi thứ còn lại là static serving thuần túy.

So sánh

Tiêu chí Django tự đọc file X-Accel-Redirect
Worker Bị chiếm suốt quá trình truyền Giải phóng ngay sau auth check
Memory Load file vào RAM (hoặc tự chunk) Nginx sendfile, zero-copy
Throughput Giới hạn bởi số worker Giới hạn bởi disk/network
Path thật của file Dễ lộ qua lỗi/log Ẩn hoàn toàn
Quyền truy cập Tùy implement Django quyết, internal chặn đường vòng

Lưu ý triển khai

  • content_type phải set đúng ở Django — nginx forward header này về client. Serve nhiều loại file thì suy ra từ extension hoặc lưu trong DB.
  • Đường dẫn trong X-Accel-Redirect phải khớp prefix với location internal — lệch một dấu / giữa alias và header là ra 404 khó hiểu; test bằng một file mẫu trước.
  • Pattern tương đương tồn tại ở Apache/Caddy dưới tên X-Sendfile — khác header, cùng ý tưởng.
  • File sinh mãi không dọn thì disk đầy: cần cleanup policy (cron xóa artifact quá hạn, hoặc TTL theo created_at), và nhớ xóa cả record DB lẫn file — mồ côi bên nào cũng phiền.

Kết

X-Accel-Redirect là ví dụ đẹp của nguyên tắc "mỗi tầng làm đúng việc của nó": application server quyết định ai được xem gì, reverse proxy lo đẩy bytes. Không thêm hạ tầng mới, không thêm dependency — chỉ một header và một location block, đổi lại throughput serve file tăng cả bậc độ lớn.