-
Couldn't load subscription status.
- Fork 373
Description
🧩 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:
-
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:
A cleaner, more expressive approach would be to override it:
const blob = await response.blob(); const fixedBlob = new Blob([blob], { type: 'image/jpeg' });
const blob = await response.blob({ type: 'image/jpeg' });
-
Creating files from fetched data
- Many applications convert responses into
Fileobjects for convenience. - This currently involves manually setting name/type/lastModified, which leads to unnecessary boilerplate:
Proposed:
const blob = await response.blob(); const file = new File([blob], 'image.jpg', { type: 'image/jpeg', lastModified: 1639094400000 });
const file = await response.file({ type: 'image/jpeg', name: 'image.jpg', lastModified: 1639094400000 });
- Many applications convert responses into
-
Automatic metadata inference
- Developers often try to extract
filenamefromContent-Dispositionheaders, and fallback to parsing the URL. - This logic is duplicated across countless apps.
- Developers often try to extract
✅ 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-Typeheader or Blob's type
-
lastModified:- If passed in options, use that
- Else from
Last-Modifiedheader (if accessible) - Else fallback to
Date.now()
⚠️ CORS Considerations
-
Inference relies on access to headers such as:
Content-DispositionContent-TypeLast-Modified
-
These must be explicitly exposed using
Access-Control-Expose-Headersby 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
-
📦 Read the response body as a Blob
Callsresponse.blob()to get the raw binary data from the response. -
📥 Retrieve the
Content-Dispositionheader
This header often contains the suggested filename for downloaded files. -
📄 Extract filename from the
Content-Dispositionheader if present
Uses a regular expression to handle both standardfilename=and RFC 5987 encodedfilename*=UTF-8''...formats to extract the filename. -
🔗 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. -
🧪 Determine the MIME type
The MIME type is determined in the following priority order:- The explicit
typeoption passed to the function, if any - The Blob's inherent MIME type (
blob.type) - The
Content-Typeheader from the response - Fallback to empty string if none of the above are available
- The explicit
-
🕒 Determine the
lastModifiedtimestamp
The last modified time is determined in the following priority order:- The explicit
lastModifiedoption passed to the function, if any - The parsed
Last-Modifiedheader from the response, if available and valid - Fallback to the current timestamp (
Date.now()) if no valid header or option is provided
- The explicit
-
🗂 Create and return the
Fileobject
Constructs a newFileusing 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.