hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses File Uploads and Storage with @hectoday/http

The Basics

  • Why File Uploads Are Hard
  • Project Setup

Receiving Files

  • Multipart Form Data
  • Validating Uploads
  • Saving to Disk

Serving Files

  • Serving Static Files
  • Range Requests and Resumable Downloads
  • Access Control on Files

Production Patterns

  • Streaming Uploads
  • Image Processing
  • Upload Progress and Cancellation

Cloud Storage

  • Presigned URLs
  • Moving from Local to Cloud

Putting It All Together

  • File Upload Checklist
  • Capstone: File Sharing API

Range Requests and Resumable Downloads

Why range requests exist

A user is downloading a 500 MB video. At 90%, the connection drops. Without range requests, they start over from the beginning. With range requests, they resume from byte 450,000,000.

Range requests also enable video seeking. When a user clicks the middle of a video timeline, the browser requests only the bytes starting at that point — it does not download the entire file first.

How it works

The client sends a Range header:

GET /files/abc/download HTTP/1.1
Range: bytes=450000000-

This means: “Send me bytes starting from 450,000,000 to the end.”

The server responds with 206 Partial Content (not 200) and a Content-Range header:

HTTP/1.1 206 Partial Content
Content-Range: bytes 450000000-499999999/500000000
Content-Length: 50000000

The browser knows it received part of the file and can combine it with the part it already has.

Implementation

route.get("/files/:id/download", {
  resolve: (c) => {
    const file = db
      .prepare("SELECT stored_name, original_name, mime_type FROM files WHERE id = ?")
      .get(c.params.id) as any;
    if (!file) return Response.json({ error: "File not found" }, { status: 404 });

    const filePath = join(UPLOAD_DIR, file.stored_name);
    let stat;
    try {
      stat = statSync(filePath);
    } catch {
      return Response.json({ error: "File not found" }, { status: 404 });
    }

    const totalSize = stat.size;
    const rangeHeader = c.request.headers.get("range");

    if (rangeHeader) {
      // Parse the range header
      const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
      if (!match) {
        return new Response("Invalid range", { status: 416 });
      }

      const start = parseInt(match[1], 10);
      const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;

      if (start >= totalSize || end >= totalSize || start > end) {
        return new Response("Range not satisfiable", {
          status: 416,
          headers: { "content-range": `bytes */${totalSize}` },
        });
      }

      const chunkSize = end - start + 1;
      const nodeStream = createReadStream(filePath, { start, end });
      const webStream = Readable.toWeb(nodeStream) as ReadableStream;

      return new Response(webStream, {
        status: 206,
        headers: {
          "content-type": file.mime_type,
          "content-length": String(chunkSize),
          "content-range": `bytes ${start}-${end}/${totalSize}`,
          "accept-ranges": "bytes",
        },
      });
    }

    // No range header — send the full file
    const nodeStream = createReadStream(filePath);
    const webStream = Readable.toWeb(nodeStream) as ReadableStream;

    return new Response(webStream, {
      headers: {
        "content-type": file.mime_type,
        "content-length": String(totalSize),
        "content-disposition": `attachment; filename="${encodeURIComponent(file.original_name)}"`,
        "accept-ranges": "bytes",
      },
    });
  },
});

Key details:

accept-ranges: bytes tells the client that the server supports range requests. Without this header, the client does not know it can request ranges.

Status 206 indicates a partial response. The client uses Content-Range to know which bytes it received and how many remain.

Status 416 (Range Not Satisfiable) is returned when the requested range is invalid (start beyond file size, end before start).

createReadStream({ start, end }) reads only the requested byte range from disk. No wasted I/O.

Video streaming

Video players (HTML5 <video>, mobile players) use range requests automatically. When a user seeks to the middle of a video, the browser sends a range request for the bytes at that position.

<video src="/files/abc/download" controls></video>

This works out of the box if your server supports range requests. The browser handles the seeking, buffering, and range negotiation.

Exercises

Exercise 1: Upload a large file (any file over 1 MB). Download it with a range header: curl -H "Range: bytes=0-1023" http://localhost:3000/files/ID/download. Verify you get 206 and 1024 bytes.

Exercise 2: Request a range beyond the file size. Verify you get 416.

Exercise 3: Upload a video file. Serve it at /files/ID/download. Open the URL in a browser with a <video> tag. Verify seeking works (clicking the timeline jumps to the right position).

Why does the server return 206 instead of 200 for range requests?

← Serving Static Files Access Control on Files →

© 2026 hectoday. All rights reserved.