- From: Jimmy Wärting <notifications@github.com>
- Date: Mon, 16 Jun 2025 09:55:05 -0700
- To: whatwg/fetch <fetch@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/1836@github.com>
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