Lập trình · 02/10/2026

HTTP Range Requests thực chiến: Tải tiếp file lớn đúng chuẩn

Một file 8 GB bị ngắt ở byte thứ 6,4 tỷ không nên phải tải lại từ đầu. HTTP Range Requests cho phép client yêu cầu đúng một đoạn của representation bằng header Range. Khi triển khai đúng, cơ chế này giúp tải tiếp file, tua video, đọc từng phần tài liệu lớn và tối ưu cache ở edge. Khi triển khai sai, nó tạo file ghép bị hỏng, rò rỉ dữ liệu hoặc biến một request thành hàng nghìn lần seek đĩa.

HTTP Range Requests thực chiến: Tải tiếp file lớn đúng chuẩn

Một file 8 GB bị ngắt ở byte thứ 6,4 tỷ không nên phải tải lại từ đầu. HTTP Range Requests cho phép client yêu cầu đúng một đoạn của representation bằng header Range. Khi triển khai đúng, cơ chế này giúp tải tiếp file, tua video, đọc từng phần tài liệu lớn và tối ưu cache ở edge. Khi triển khai sai, nó tạo file ghép bị hỏng, rò rỉ dữ liệu hoặc biến một request thành hàng nghìn lần seek đĩa.

Bài này đi từ semantics của RFC 9110 đến một handler PHP hỗ trợ single range, cách dùng If-Range, kiểm thử bằng curl và các guardrail cần có trước khi đưa lên production.

1. Range request giải quyết bài toán nào?

Client gửi một request GET bình thường khi cần toàn bộ file. Nếu chỉ cần một đoạn, nó thêm đơn vị bytes:

GET /downloads/archive.zip HTTP/1.1
Host: files.example.com
Range: bytes=1048576-2097151

Server có thể trả đúng 1 MiB được yêu cầu:

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 1048576-2097151/8589934592
Content-Length: 1048576
Content-Type: application/zip
ETag: "sha256:8f..."

[1 MiB dữ liệu]

Content-Length ở đây là kích thước phần được gửi, không phải kích thước toàn file. Tổng kích thước nằm sau dấu gạch chéo trong Content-Range. Client chỉ được ghép dữ liệu khi validator chứng minh các đoạn thuộc cùng một phiên bản.

2. Ba cú pháp byte-range cần hỗ trợ

HeaderÝ nghĩaVới file 10.000 byte
bytes=0-999Từ vị trí 0 đến 999, bao gồm cả hai đầu1.000 byte đầu
bytes=7000-Từ vị trí 7.000 đến hết3.000 byte cuối
bytes=-500Suffix range500 byte cuối

Chỉ số byte bắt đầu từ 0 và last-pos là inclusive. Vì vậy bytes=0-0 hợp lệ và yêu cầu đúng một byte. Lỗi lệch một byte thường xuất hiện khi lập trình viên tính length = end - start thay vì end - start + 1.

Nếu end vượt quá cuối file nhưng start vẫn hợp lệ, server giới hạn end về size - 1. Nếu suffix dài hơn file, toàn bộ representation có thể được trả dưới dạng 206. Đừng dùng số nguyên 32-bit cho file lớn; RFC yêu cầu parser phòng chống overflow từ các chữ số thập phân rất dài.

3. Phân biệt 200, 206 và 416

  • 200 OK: trả toàn bộ representation khi không có Range, khi server không hỗ trợ Range, hoặc khi server chủ động bỏ qua header hợp lệ.
  • 206 Partial Content: Range hợp lệ, thỏa mãn và server gửi một hay nhiều phần.
  • 416 Range Not Satisfiable: range hợp lệ về cú pháp nhưng không giao với representation hiện tại, ví dụ bytes=20000- cho file 10.000 byte.
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */10000

Với header sai cú pháp hoặc đơn vị không hỗ trợ, server có thể bỏ qua Range và xử lý như GET thông thường. Đừng trả 206 nếu body thực tế là toàn bộ file; cache và client sẽ ghép sai dữ liệu.

4. Accept-Ranges là tín hiệu, không phải lời hứa tuyệt đối

Accept-Ranges: bytes

Header này báo rằng upstream hỗ trợ byte ranges cho resource. Client vẫn có thể thử Range khi chưa thấy Accept-Ranges, và cũng không được giả định mọi request sau chắc chắn nhận 206: object có thể thay đổi, quyền truy cập có thể khác hoặc một intermediary khác xử lý request.

Nếu endpoint không hỗ trợ range, có thể gửi Accept-Ranges: none. Với download endpoint đã công bố hỗ trợ, hãy trả Accept-Ranges: bytes nhất quán trên response 200, 206 và HEAD để client dễ lập kế hoạch.

5. If-Range ngăn ghép hai phiên bản khác nhau

Giả sử client đã có byte 0–999 của bản A. Trong lúc kết nối gián đoạn, server thay file bằng bản B. Nếu client tải byte 1000– cuối của bản B rồi nối vào phần cũ, checksum thất bại dù từng request đều thành công.

Range: bytes=1000-
If-Range: "sha256:8f..."

Nếu validator vẫn khớp, server áp dụng Range và trả 206. Nếu không khớp, server bỏ qua Range và trả toàn bộ bản mới bằng 200. Client nhờ đó thay file cũ thay vì nối nhầm.

If-Range chấp nhận strong ETag hoặc HTTP-date. Weak ETag như W/"v7" không phù hợp vì tải tiếp cần đồng nhất từng byte. Ngày sửa đổi chỉ đáng tin khi hệ thống có độ chính xác timestamp phù hợp; content digest hoặc version ID bất biến thường an toàn hơn.

6. Single range và multiple ranges

Một request có thể hỏi nhiều đoạn:

Range: bytes=0-99,1000-1099

Response đúng phải dùng multipart/byteranges, có boundary và Content-Range riêng trong từng part. Đây là phần dễ lỗi và hiếm khi cần cho endpoint tải file thông thường. Server được phép bỏ qua hoặc từ chối tập nhiều range bất thường, chồng lấn hoặc gồm nhiều đoạn nhỏ vì chúng có thể là dấu hiệu DoS.

Một policy thực dụng là chỉ hỗ trợ single range, bỏ qua header có dấu phẩy và trả 200 toàn file. Nếu sản phẩm thật sự cần multi-range, đặt giới hạn số đoạn, gộp vùng chồng lấn/kề nhau, tính trước overhead và kiểm thử parser multipart độc lập.

7. Handler PHP hỗ trợ single range an toàn hơn

Ví dụ dưới đây cố ý chỉ hỗ trợ một range. Authorization và ánh xạ object ID sang đường dẫn canonical phải hoàn tất trước đoạn này; không nối trực tiếp input của user vào filesystem path.

<?php
declare(strict_types=1);

$path = '/srv/downloads/release-2026.10.zip';
$size = filesize($path);
$etag = '"sha256:' . getStoredDigest($path) . '"';
$start = 0;
$end = $size - 1;
$partial = false;

header('Accept-Ranges: bytes');
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="release.zip"');
header('ETag: ' . $etag);

$range = $_SERVER['HTTP_RANGE'] ?? null;
$ifRange = $_SERVER['HTTP_IF_RANGE'] ?? null;

// Ví dụ này bỏ qua multi-range và If-Range không khớp.
$canApply = $range !== null
    && !str_contains($range, ',')
    && ($ifRange === null || hash_equals($etag, trim($ifRange)));

if ($canApply && preg_match('/^bytes=(\d*)-(\d*)$/', trim($range), $m)) {
    if ($m[1] === '' && $m[2] === '') {
        $canApply = false;
    } elseif ($m[1] === '') {
        $suffix = min((int) $m[2], $size);
        if ($suffix > 0) {
            $start = $size - $suffix;
            $partial = true;
        }
    } else {
        $start = (int) $m[1];
        $end = $m[2] === '' ? $size - 1 : min((int) $m[2], $size - 1);
        $partial = $start < $size && $start <= $end;
    }

    if (!$partial) {
        http_response_code(416);
        header("Content-Range: bytes */{$size}");
        header('Content-Length: 0');
        exit;
    }
}

$length = $end - $start + 1;
if ($partial) {
    http_response_code(206);
    header("Content-Range: bytes {$start}-{$end}/{$size}");
}
header("Content-Length: {$length}");

if ($_SERVER['REQUEST_METHOD'] === 'HEAD') {
    exit;
}

$fp = fopen($path, 'rb');
fseek($fp, $start);
$remaining = $length;
while ($remaining > 0 && !feof($fp)) {
    $chunk = fread($fp, min(1024 * 1024, $remaining));
    if ($chunk === false) break;
    echo $chunk;
    $remaining -= strlen($chunk);
    flush();
}
fclose($fp);

Production cần bổ sung kiểm tra lỗi I/O, client disconnect, phương thức chỉ GET/HEAD, số nguyên lớn theo runtime, logging và bộ so sánh HTTP-date nếu chấp nhận date validator. getStoredDigest() ám chỉ digest đã lưu cùng object; băm lại file nhiều GB trên mỗi request sẽ phá lợi ích của streaming.

8. Không nạp toàn bộ file vào RAM

file_get_contents() rồi substr() tạo peak memory theo kích thước file. Handler nên seek đến offset và stream theo chunk cố định, hoặc ủy quyền cho web server/object storage sau khi đã xác thực quyền.

  • Nginx: dùng X-Accel-Redirect tới internal location sau authorization.
  • Apache: cân nhắc X-Sendfile nếu module và threat model phù hợp.
  • Object storage: trả signed URL ngắn hạn, ràng buộc đúng object/version.
  • Ứng dụng: stream chunk, không buffer toàn body và xử lý backpressure.

Kernel/web server thường phục vụ byte ranges và zero-copy hiệu quả hơn application runtime. Dù ủy quyền, lớp ứng dụng vẫn phải kiểm tra tenant, entitlement và object version trước khi mở quyền tải.

9. Content-Encoding làm offset khó hiểu

Range áp dụng lên dữ liệu của selected representation. Nếu cùng URL đôi lúc trả gzip, đôi lúc identity, offset byte và tổng chiều dài có thể khác. Một client không được nối byte đã tải từ identity với byte của gzip.

Với file tải xuống lớn, cách đơn giản là dùng object/version URL ổn định, ETag mạnh, Vary: Accept-Encoding đúng khi có negotiation và đảm bảo mọi phần của một phiên tải dùng cùng content coding. Không cần tắt compression toàn hệ thống; cần làm representation và validator nhất quán.

10. CDN và cache partial content

CDN có thể chuyển tiếp Range về origin, cache 206, hoặc tự cắt range từ object đầy đủ đã cache. Hành vi khác nhau theo nhà cung cấp và cấu hình. Nếu dùng Nginx slice để cache file lớn, tài liệu chính thức yêu cầu đưa $slice_range vào cache key và bật cache cho 206.

Các lỗi cấu hình thường gặp:

  • Cache key không phân biệt slice, khiến đoạn này được trả cho đoạn khác.
  • Origin trả 200 nhưng proxy giữ Content-Range cũ.
  • CDN nén response khác origin nhưng tái sử dụng ETag không còn mạnh theo byte.
  • Signed URL hết hạn giữa quá trình resume.
  • Cache phục vụ phiên bản cũ trong khi metadata API công bố kích thước mới.

Kiểm thử cả đường MISS, HIT, revalidation và purge; đừng chỉ gọi thẳng origin.

11. Guardrail bảo mật và chống lạm dụng

  • Authorization trước khi parse/stream Range; 206 không được trở thành lối vòng qua ACL.
  • Ánh xạ ID sang path từ dữ liệu tin cậy, chống path traversal và symlink escape.
  • Giới hạn số range; với endpoint single-range, bỏ qua mọi header có dấu phẩy.
  • Giới hạn độ dài header và parse chữ số với kiểm tra overflow.
  • Rate limit theo user/IP/object, đặc biệt các range rất nhỏ và ngẫu nhiên.
  • Không tiết lộ kích thước object cho người chưa có quyền qua response 416.
  • Giới hạn thời gian, băng thông và số stream đồng thời; dừng đọc khi client ngắt.

Range có thể khuếch đại chi phí: body chỉ vài byte nhưng server phải mở file, seek, giải mã hoặc gọi storage backend. Theo dõi tỷ lệ byte phục vụ trên số request thay vì chỉ tổng bandwidth.

12. Bộ kiểm thử tối thiểu bằng curl

# Header của file đầy đủ
curl -sSI https://files.example.com/archive.zip

# 100 byte đầu: kỳ vọng 206 và Content-Range bytes 0-99/TOTAL
curl -sS -D headers.txt -H "Range: bytes=0-99" \
  https://files.example.com/archive.zip -o first-100.bin

# Range vượt kích thước: kỳ vọng 416 và Content-Range bytes */TOTAL
curl -sS -D - -H "Range: bytes=999999999999-" \
  https://files.example.com/archive.zip -o /dev/null

# Resume tự động của curl từ kích thước file cục bộ
curl -C - -O https://files.example.com/archive.zip

Ngoài happy path, hãy test bytes=0-0, bytes=-1, open-ended range, end vượt size, start bằng size, range đảo ngược, nhiều dấu phẩy, số cực lớn, file rỗng, HEAD, ETag đúng/sai và object đổi phiên bản giữa hai request.

13. Xác minh file ghép, không chỉ status code

Một response 206 đẹp mắt vẫn có thể gửi sai offset. Test tích hợp nên:

  1. Tải toàn file và tính SHA-256 làm chuẩn.
  2. Tải cùng file thành nhiều single range ở các ranh giới không tròn.
  3. Ghép theo thứ tự byte.
  4. So sánh kích thước và SHA-256 với file chuẩn.
  5. Lặp lại qua CDN HIT/MISS và khi client hủy giữa chừng.

Đồng thời kiểm tra Content-Length = end - start + 1, body đúng số byte, Content-Range đúng total và 416 không chứa dữ liệu ngoài ý muốn.

14. Observability cần cho production

Log có cấu trúc nên chứa object ID/version, status 200/206/416, start, end, total size, byte đã gửi thật, ETag, cache status, thời gian tới byte đầu, thời gian stream và lý do kết thúc. Tránh log signed URL hoặc token.

Các chỉ số hữu ích gồm:

  • Tỷ lệ 206/200 và 416 theo client version.
  • Phân phối kích thước range, số request trên một download và resume success rate.
  • Client abort rate, timeout, read error và storage latency.
  • CDN hit ratio cho full object và partial response.
  • Số header multi-range/không hợp lệ bị bỏ qua hoặc chặn.

416 tăng đột biến sau một bản phát hành có thể là bug client lưu sai offset, không chỉ traffic xấu. 200 tăng trong flow resume có thể báo ETag thay đổi quá thường xuyên hoặc intermediary đã strip Range.

15. Checklist trước khi bật tải tiếp

  • GET đầy đủ trả 200; single range thỏa mãn trả 206; range ngoài file trả 416.
  • Content-Length, Content-Range và body khớp tuyệt đối.
  • Strong ETag hoặc version validator ổn định; If-Range sai trả full 200.
  • Authorization, canonical path và object version được khóa trước khi stream.
  • Không buffer toàn file; có backpressure và dừng khi client disconnect.
  • Policy multi-range, giới hạn header, overflow và rate limit đã rõ.
  • Content-Encoding, CDN cache key, purge và signed URL được kiểm thử.
  • File ghép qua nhiều range có cùng checksum với file gốc.
  • Dashboard theo dõi 200/206/416, abort, cache hit và byte thực gửi.

Range Requests không chỉ là thêm status 206. Một hệ thống tải tiếp đáng tin cậy cần giữ nguyên identity của representation, tính đúng từng byte, giới hạn workload do client điều khiển và kiểm chứng file cuối bằng checksum. Bắt đầu với single range, validator mạnh và test qua đúng CDN path trước khi cân nhắc multipart.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.