[whatwg/fetch] Feature Request: `response.blob({ type })` and `response.file({ type?, name?, lastModified? })` (Issue #1836)

jimmywarting created an issue (whatwg/fetch#1836)

### 🧩 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:
     ```js
     const blob = await response.blob();
     const fixedBlob = new Blob([blob], { type: 'image/jpeg' });
     ```
     A cleaner, more expressive approach would be to override it:
     ```js
     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:
     ```js
     const blob = await response.blob();
     const file = new File([blob], 'image.jpg', {
       type: 'image/jpeg',
       lastModified: 1639094400000
     });
     ```
     Proposed:
     ```js
     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`:
  - From `Content-Disposition: attachment; filename=...` (if accessible)
  - Else from the last segment of `response.url`
  - Else `"download"`

- `type`:
  - From `Content-Type` header or Blob's type
  - Or overridden

- `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:

```js
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') || 'application/octet-stream';

  // 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 `'application/octet-stream'` 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.

-- 
Reply to this email directly or view it on GitHub:
https://github.com/whatwg/fetch/issues/1836
You are receiving this because you are subscribed to this thread.

Message ID: <whatwg/fetch/issues/1836@github.com>

Received on Monday, 16 June 2025 16:55:09 UTC