From dd75beb8ad3c54424625eacef3c0d416626d8f66 Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Tue, 7 May 2024 02:35:00 +0530 Subject: [PATCH] feat(http): support sending files along with json (#522) --- docs/network-requests/http.md | 11 ++++++++++ lib/http/Http.ts | 40 +++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/docs/network-requests/http.md b/docs/network-requests/http.md index ba3c58e3..c54bd2ee 100644 --- a/docs/network-requests/http.md +++ b/docs/network-requests/http.md @@ -54,6 +54,17 @@ interface HttpOptions { * @default undefined */ lang?: 'en' | 'ja' + /** + * If you call `http.post` with a file, it will be send as `multipart/form-data`. + * The rest of the body will be send as JSON string. This option allows you to + * specify the key for the JSON part. This key should match the key in backend + * middleware which parses the JSON part. Don't set this option to some common + * key to avoid conflicts with other parts of the body. (Sending JSON part as + * string is needed to preserve data types.) + * + * @default '__payload__' + */ + payloadKey?: string } interface HttpClient { diff --git a/lib/http/Http.ts b/lib/http/Http.ts index e8215ead..c9ea7167 100644 --- a/lib/http/Http.ts +++ b/lib/http/Http.ts @@ -15,6 +15,7 @@ export interface HttpOptions { xsrfUrl?: string | false client?: HttpClient lang?: Lang + payloadKey?: string } export class Http { @@ -22,6 +23,7 @@ export class Http { private static xsrfUrl: string | false = '/api/csrf-cookie' private static client: HttpClient = ofetch private static lang: Lang | undefined = undefined + private static payloadKey = '__payload__' static config(options: HttpOptions) { if (options.baseUrl) { @@ -36,6 +38,9 @@ export class Http { if (options.lang) { Http.lang = options.lang } + if (options.payloadKey) { + Http.payloadKey = options.payloadKey + } } private async ensureXsrfToken(): Promise { @@ -101,6 +106,26 @@ export class Http { } async post(url: string, body?: any, options?: FetchOptions): Promise { + if (body && !(body instanceof FormData)) { + let hasFile = false + + const payload = JSON.stringify(body, (_, value) => { + if (value instanceof Blob) { + hasFile = true + return undefined + } + return value + }) + + if (hasFile) { + const formData = this.objectToFormData(body, undefined, undefined, true) + formData.append(Http.payloadKey, payload) + body = formData + } else { + body = payload + } + } + return this.performRequest(url, { method: 'POST', body, ...options }) } @@ -117,13 +142,7 @@ export class Http { } async upload(url: string, body?: any, options?: FetchOptions): Promise { - const formData = this.objectToFormData(body) - - return this.performRequest(url, { - method: 'POST', - body: formData, - ...options - }) + return this.post(url, this.objectToFormData(body), options) } async download(url: string, options?: FetchOptions): Promise { @@ -143,7 +162,7 @@ export class Http { FileSaver.saveAs(blob, filename as string) } - private objectToFormData(obj: any, form?: FormData, namespace?: string) { + private objectToFormData(obj: any, form?: FormData, namespace?: string, onlyFiles = false) { const fd = form || new FormData() let formKey: string @@ -163,9 +182,12 @@ export class Http { && !(obj[property] instanceof Blob) && obj[property] !== null ) { - this.objectToFormData(obj[property], fd, property) + this.objectToFormData(obj[property], fd, property, onlyFiles) } else { const value = obj[property] === null ? '' : obj[property] + if (onlyFiles && !(value instanceof Blob)) { + return + } fd.append(formKey, value) } })