Skip to content

Bug: getObjectRaw() generates invalid Range header when rangeTo is undefined #49

@leonardas103

Description

@leonardas103

Description

When using getObjectRaw(key, wholeFile, rangeFrom, rangeTo) with rangeTo set to undefined for open-ended HTTP range requests (e.g., bytes=8388608-), s3mini generates an invalid Range header that causes S3 to return a 400 - InvalidArgument error.

Environment

  • s3mini version: 0.7.1
  • S3 Provider: RustFS (S3-compatible)

Steps to Reproduce

  1. Call getObjectRaw() with explicit rangeFrom but undefined rangeTo:
const s3client = new S3mini({...});

// Attempt to fetch from byte 8388608 to end of file
const response = await s3client.getObjectRaw(
  "video.mp4",
  false,        // wholeFile = false
  8388608,      // rangeFrom = 8388608
  undefined     // rangeTo = undefined (should mean "to end of file")
);
  1. S3 responds with error:
<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>InvalidArgument</Code>
  <Message>invalid header: range: "bytes=8388608-8388607"</Message>
</Error>

Expected Behavior

When rangeTo is undefined, s3mini should generate an open-ended range header:

Range: bytes=8388608-

This is a standard HTTP range request meaning "from byte 8388608 to the end of the file."

Actual Behavior

s3mini generates an invalid range header where the end byte is LESS than the start byte:

Range: bytes=8388608-8388607

This causes S3 to reject the request with 400 - InvalidArgument.

Root Cause

Found in the getObjectRaw() method (lines 1019-1032 in the source):

public async getObjectRaw(
  key: string,
  wholeFile = true,
  rangeFrom = 0,
  rangeTo = this.requestSizeInBytes,  // ← Default value problem
  opts: Record<string, unknown> = {},
  ssecHeaders?: IT.SSECHeaders,
): Promise<Response> {
  const rangeHdr: Record<string, string | number> = wholeFile
    ? {}
    : { range: `bytes=${rangeFrom}-${rangeTo - 1}` };  // ← Always subtracts 1
  // ...
}

The Problem:

  1. The rangeTo parameter has a default value of this.requestSizeInBytes (8388608 bytes = 8MB)
  2. When you call getObjectRaw(key, false, 8388608, undefined), JavaScript/TypeScript doesn't pass undefined as the default value—it uses the parameter's default: this.requestSizeInBytes
  3. This results in rangeTo = 8388608 when rangeFrom = 8388608
  4. The range header becomes: bytes=8388608-${8388608-1} = bytes=8388608-8388607
  5. This is invalid because end byte < start byte

The function has no way to detect open-ended ranges because undefined gets replaced by the default value before the function body executes.

Proposed Fix

Change the function signature to make rangeTo optional without a default value:

public async getObjectRaw(
  key: string,
  wholeFile = true,
  rangeFrom = 0,
  rangeTo?: number,  // ← Remove default value, make optional
  opts: Record<string, unknown> = {},
  ssecHeaders?: IT.SSECHeaders,
): Promise<Response> {
  let rangeHdr: Record<string, string | number> = {};
  
  if (!wholeFile) {
    if (rangeTo !== undefined) {
      // Closed range with explicit end
      rangeHdr = { range: `bytes=${rangeFrom}-${rangeTo - 1}` };
    } else {
      // Open-ended range: from rangeFrom to end of file
      rangeHdr = { range: `bytes=${rangeFrom}-` };
    }
  }

  return this._signedRequest('GET', key, {
    query: { ...opts },
    headers: { ...rangeHdr, ...ssecHeaders },
    withQuery: true,
  });
}

Why this works:

  • Removes the default value from rangeTo, making it truly optional
  • Explicitly checks for undefined to create open-ended ranges
  • Maintains backward compatibility for callers who provide explicit values
  • Fixes the invalid range generation bug

Workaround

Currently, users must fetch the file size first and provide it explicitly:

// 1. Get file size via small range request
const sizeCheck = await s3client.getObjectRaw(key, false, 0, 1);
const contentRange = sizeCheck.headers.get("content-range");
const fileSize = parseInt(contentRange.match(/bytes \d+-\d+\/(\d+)/)[1]);

// 2. Now fetch with explicit end byte
const response = await s3client.getObjectRaw(
  key,
  false,
  8388608,
  fileSize  // Must provide explicit file size instead of undefined
);

Use Case

This bug affects video streaming implementations where browsers make open-ended range requests as users watch videos. A typical video playback session generates requests like:

  • GET /video.mp4Range: bytes=0-
  • (user seeks to 30 seconds)
  • GET /video.mp4Range: bytes=8388608-

Without support for open-ended ranges, video seeking breaks.

Additional Context

The HTTP/1.1 specification (RFC 7233) defines open-ended range requests:

A client can limit the number of bytes requested without knowing the size of the selected representation. If the last-byte-pos value is absent, or if the value is greater than or equal to the current length of the representation data, the byte range is interpreted as the remainder of the representation (i.e., the server replaces the value of last-byte-pos with a value that is one less than the current length of the selected representation).

Source: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1

S3-compatible storage services follow this standard and expect open-ended ranges to work.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions