- 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