Skip to content

Commit dd75beb

Browse files
authored
feat(http): support sending files along with json (#522)
1 parent 7f2abd1 commit dd75beb

File tree

2 files changed

+42
-9
lines changed

2 files changed

+42
-9
lines changed

docs/network-requests/http.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ interface HttpOptions {
5454
* @default undefined
5555
*/
5656
lang?: 'en' | 'ja'
57+
/**
58+
* If you call `http.post` with a file, it will be send as `multipart/form-data`.
59+
* The rest of the body will be send as JSON string. This option allows you to
60+
* specify the key for the JSON part. This key should match the key in backend
61+
* middleware which parses the JSON part. Don't set this option to some common
62+
* key to avoid conflicts with other parts of the body. (Sending JSON part as
63+
* string is needed to preserve data types.)
64+
*
65+
* @default '__payload__'
66+
*/
67+
payloadKey?: string
5768
}
5869

5970
interface HttpClient {

lib/http/Http.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ export interface HttpOptions {
1515
xsrfUrl?: string | false
1616
client?: HttpClient
1717
lang?: Lang
18+
payloadKey?: string
1819
}
1920

2021
export class Http {
2122
private static baseUrl: string | undefined = undefined
2223
private static xsrfUrl: string | false = '/api/csrf-cookie'
2324
private static client: HttpClient = ofetch
2425
private static lang: Lang | undefined = undefined
26+
private static payloadKey = '__payload__'
2527

2628
static config(options: HttpOptions) {
2729
if (options.baseUrl) {
@@ -36,6 +38,9 @@ export class Http {
3638
if (options.lang) {
3739
Http.lang = options.lang
3840
}
41+
if (options.payloadKey) {
42+
Http.payloadKey = options.payloadKey
43+
}
3944
}
4045

4146
private async ensureXsrfToken(): Promise<string | undefined> {
@@ -101,6 +106,26 @@ export class Http {
101106
}
102107

103108
async post<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
109+
if (body && !(body instanceof FormData)) {
110+
let hasFile = false
111+
112+
const payload = JSON.stringify(body, (_, value) => {
113+
if (value instanceof Blob) {
114+
hasFile = true
115+
return undefined
116+
}
117+
return value
118+
})
119+
120+
if (hasFile) {
121+
const formData = this.objectToFormData(body, undefined, undefined, true)
122+
formData.append(Http.payloadKey, payload)
123+
body = formData
124+
} else {
125+
body = payload
126+
}
127+
}
128+
104129
return this.performRequest<T>(url, { method: 'POST', body, ...options })
105130
}
106131

@@ -117,13 +142,7 @@ export class Http {
117142
}
118143

119144
async upload<T = any>(url: string, body?: any, options?: FetchOptions): Promise<T> {
120-
const formData = this.objectToFormData(body)
121-
122-
return this.performRequest<T>(url, {
123-
method: 'POST',
124-
body: formData,
125-
...options
126-
})
145+
return this.post<T>(url, this.objectToFormData(body), options)
127146
}
128147

129148
async download(url: string, options?: FetchOptions): Promise<void> {
@@ -143,7 +162,7 @@ export class Http {
143162
FileSaver.saveAs(blob, filename as string)
144163
}
145164

146-
private objectToFormData(obj: any, form?: FormData, namespace?: string) {
165+
private objectToFormData(obj: any, form?: FormData, namespace?: string, onlyFiles = false) {
147166
const fd = form || new FormData()
148167
let formKey: string
149168

@@ -163,9 +182,12 @@ export class Http {
163182
&& !(obj[property] instanceof Blob)
164183
&& obj[property] !== null
165184
) {
166-
this.objectToFormData(obj[property], fd, property)
185+
this.objectToFormData(obj[property], fd, property, onlyFiles)
167186
} else {
168187
const value = obj[property] === null ? '' : obj[property]
188+
if (onlyFiles && !(value instanceof Blob)) {
189+
return
190+
}
169191
fd.append(formKey, value)
170192
}
171193
})

0 commit comments

Comments
 (0)