Skip to content

Feature Request: response.blob({ type }) and response.file({ type?, name?, lastModified? })Β #1836

Open
@jimmywarting

Description

@jimmywarting

🧩 Motivation

There are several real-world scenarios where it is useful to reinterpret the content of a Response and create a Blob or File with corrected metadata, especially in cases like:

  1. Partial content (Range requests)

    • You might fetch a byte range inside a ZIP file that contains a JPEG.
    • The response will still have Content-Type: application/zip, even though the actual data is a JPEG.
    • The workaround today looks like this:
      const blob = await response.blob();
      const fixedBlob = new Blob([blob], { type: 'image/jpeg' });
      A cleaner, more expressive approach would be to override it:
      const blob = await response.blob({ type: 'image/jpeg' });
  2. Creating files from fetched data

    • Many applications convert responses into File objects for convenience.
    • This currently involves manually setting name/type/lastModified, which leads to unnecessary boilerplate:
      const blob = await response.blob();
      const file = new File([blob], 'image.jpg', {
        type: 'image/jpeg',
        lastModified: 1639094400000
      });
      Proposed:
      const file = await response.file({
        type: 'image/jpeg',
        name: 'image.jpg',
        lastModified: 1639094400000
      });
  3. Automatic metadata inference

    • Developers often try to extract filename from Content-Disposition headers, and fallback to parsing the URL.
    • This logic is duplicated across countless apps.

βœ… Suggested Behavior

response.blob({ type })

Returns a Blob with the same binary content, but overrides its MIME type if specified.

response.file({ type?, name?, lastModified? })

Returns a File object, with metadata inferred from the response (or overridden by options):

  • name:

    • If passed in options, use that
    • From Content-Disposition: attachment; filename=... (if accessible)
    • Else from the last segment of response.url
    • Else "download"
  • type:

    • If passed in options, use that
    • From Content-Type header or Blob's type
  • lastModified:

    • If passed in options, use that
    • Else from Last-Modified header (if accessible)
    • Else fallback to Date.now()

⚠️ CORS Considerations

  • Inference relies on access to headers such as:

    • Content-Disposition
    • Content-Type
    • Last-Modified
  • These must be explicitly exposed using Access-Control-Expose-Headers by the server.

  • If the headers are not available due to CORS restrictions, default fallback values (e.g. "download", Date.now(), or MIME type detection) must be used.

  • Developers can always override the values manually when needed.


πŸ§ͺ Polyfill Example

This shows how much code developers currently need to write to get similar functionality:

Response.prototype.file ??= async function file({ type, name, lastModified } = {}) {
  // Step 1: Read the response content as a Blob
  const blob = await this.blob();

  // Step 2: Get the Content-Disposition header
  const contentDisposition = this.headers.get('Content-Disposition');

  // Step 3: Try to extract filename from Content-Disposition
  let inferredName = 'download';
  if (contentDisposition?.includes('filename=')) {
    const match = contentDisposition.match(/filename\*?=(?:UTF-8'')?["']?([^"';\n]+)["']?/i);
    if (match?.[1]) {
      inferredName = decodeURIComponent(match[1]);
    }
  } else {
    // Step 4: If no Content-Disposition, try to extract filename from URL pathname
    try {
      const url = new URL(this.url);
      const lastSegment = url.pathname.split('/').filter(Boolean).pop();
      if (lastSegment) inferredName = lastSegment;
    } catch {
      // URL might be empty or invalid, fallback to default
    }
  }

  // Step 5: Determine the MIME type
  const inferredType = type ?? blob.type || this.headers.get('Content-Type') || '';

  // Step 6: Determine lastModified time
  let inferredLastModified = Date.now();
  if (typeof lastModified === 'number') {
    inferredLastModified = lastModified;
  } else if (this.headers.has('Last-Modified')) {
    const parsed = Date.parse(this.headers.get('Last-Modified'));
    if (!isNaN(parsed)) inferredLastModified = parsed;
  }

  // Step 7: Create and return the File object
  return new File([blob], name ?? inferredName, {
    type: inferredType,
    lastModified: inferredLastModified
  });
};

Step-by-step explanation

  1. πŸ“¦ Read the response body as a Blob
    Calls response.blob() to get the raw binary data from the response.

  2. πŸ“₯ Retrieve the Content-Disposition header
    This header often contains the suggested filename for downloaded files.

  3. πŸ“„ Extract filename from the Content-Disposition header if present
    Uses a regular expression to handle both standard filename= and RFC 5987 encoded filename*=UTF-8''... formats to extract the filename.

  4. πŸ”— If no filename is found, try to infer the filename from the URL path
    Parses the response URL and extracts the last segment of the pathname as a fallback filename.

  5. πŸ§ͺ Determine the MIME type
    The MIME type is determined in the following priority order:

    • The explicit type option passed to the function, if any
    • The Blob's inherent MIME type (blob.type)
    • The Content-Type header from the response
    • Fallback to empty string if none of the above are available
  6. πŸ•’ Determine the lastModified timestamp
    The last modified time is determined in the following priority order:

    • The explicit lastModified option passed to the function, if any
    • The parsed Last-Modified header from the response, if available and valid
    • Fallback to the current timestamp (Date.now()) if no valid header or option is provided
  7. πŸ—‚ Create and return the File object
    Constructs a new File using the Blob content and the inferred or provided metadata (name, type, lastModified) and returns it.


βœ… Benefits

Simplifies common workflows involving file download, upload, and metadata extraction.

  • Reduces duplicated code.
  • Makes fetch()-based file handling more ergonomic and expressive.
  • Fully backward compatible – no existing APIs break.

🏁 Summary
This proposal adds ergonomic, expressive APIs that:

  • Are safe with CORS
  • Require minimal internal changes
  • Reflect patterns developers already reimplement manually

It would be a welcome addition to the Fetch spec for file-oriented workflows and lower-level network data handling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    addition/proposalNew features or enhancementsneeds implementer interestMoving the issue forward requires implementers to express interest

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions