|
1 | 1 | import { type RequestOptions } from './request-options'; |
2 | | -import { type FilePropertyBag } from './builtin-types'; |
| 2 | +import type { FilePropertyBag, Fetch } from './builtin-types'; |
3 | 3 | import { isFsReadStreamLike, type FsReadStreamLike } from './shims'; |
| 4 | +import type { OpenAI } from '../client'; |
4 | 5 | import './polyfill/file.node.js'; |
5 | 6 |
|
6 | 7 | type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView; |
@@ -203,21 +204,65 @@ const isAsyncIterableIterator = (value: any): value is AsyncIterableIterator<unk |
203 | 204 | * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. |
204 | 205 | * Otherwise returns the request as is. |
205 | 206 | */ |
206 | | -export const maybeMultipartFormRequestOptions = async (opts: RequestOptions): Promise<RequestOptions> => { |
| 207 | +export const maybeMultipartFormRequestOptions = async ( |
| 208 | + opts: RequestOptions, |
| 209 | + fetch: OpenAI | Fetch, |
| 210 | +): Promise<RequestOptions> => { |
207 | 211 | if (!hasUploadableValue(opts.body)) return opts; |
208 | 212 |
|
209 | | - return { ...opts, body: await createForm(opts.body) }; |
| 213 | + return { ...opts, body: await createForm(opts.body, fetch) }; |
210 | 214 | }; |
211 | 215 |
|
212 | 216 | type MultipartFormRequestOptions = Omit<RequestOptions, 'body'> & { body: unknown }; |
213 | 217 |
|
214 | 218 | export const multipartFormRequestOptions = async ( |
215 | 219 | opts: MultipartFormRequestOptions, |
| 220 | + fetch: OpenAI | Fetch, |
216 | 221 | ): Promise<RequestOptions> => { |
217 | | - return { ...opts, body: await createForm(opts.body) }; |
| 222 | + return { ...opts, body: await createForm(opts.body, fetch) }; |
218 | 223 | }; |
219 | 224 |
|
220 | | -export const createForm = async <T = Record<string, unknown>>(body: T | undefined): Promise<FormData> => { |
| 225 | +const supportsFormDataMap = new WeakMap<Fetch, Promise<boolean>>(); |
| 226 | + |
| 227 | +/** |
| 228 | + * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending |
| 229 | + * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]". |
| 230 | + * This function detects if the fetch function provided supports the global FormData object to avoid |
| 231 | + * confusing error messages later on. |
| 232 | + */ |
| 233 | +function supportsFormData(fetchObject: OpenAI | Fetch): Promise<boolean> { |
| 234 | + const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch; |
| 235 | + const cached = supportsFormDataMap.get(fetch); |
| 236 | + if (cached) return cached; |
| 237 | + const promise = (async () => { |
| 238 | + try { |
| 239 | + const FetchResponse = ( |
| 240 | + 'Response' in fetch ? |
| 241 | + fetch.Response |
| 242 | + : (await fetch('data:,')).constructor) as typeof Response; |
| 243 | + const data = new FormData(); |
| 244 | + if (data.toString() === (await new FetchResponse(data).text())) { |
| 245 | + return false; |
| 246 | + } |
| 247 | + return true; |
| 248 | + } catch { |
| 249 | + // avoid false negatives |
| 250 | + return true; |
| 251 | + } |
| 252 | + })(); |
| 253 | + supportsFormDataMap.set(fetch, promise); |
| 254 | + return promise; |
| 255 | +} |
| 256 | + |
| 257 | +export const createForm = async <T = Record<string, unknown>>( |
| 258 | + body: T | undefined, |
| 259 | + fetch: OpenAI | Fetch, |
| 260 | +): Promise<FormData> => { |
| 261 | + if (!(await supportsFormData(fetch))) { |
| 262 | + throw new TypeError( |
| 263 | + 'The provided fetch function does not support file uploads with the current global FormData class.', |
| 264 | + ); |
| 265 | + } |
221 | 266 | const form = new FormData(); |
222 | 267 | await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); |
223 | 268 | return form; |
|
0 commit comments