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
File
objects 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
filename
fromContent-Disposition
headers, 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-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
-
π¦ Read the response body as a Blob
Callsresponse.blob()
to get the raw binary data from the response. -
π₯ Retrieve the
Content-Disposition
header
This header often contains the suggested filename for downloaded files. -
π Extract filename from the
Content-Disposition
header 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
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
- The explicit
-
π 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
- The explicit
-
π Create and return the
File
object
Constructs a newFile
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.