Skip to content

Commit 48e941c

Browse files
authored
Merge pull request #2940 from hey-api/copilot/fix-formdata-boundary-issue
2 parents 06ed518 + b290ec9 commit 48e941c

File tree

15 files changed

+313
-26
lines changed

15 files changed

+313
-26
lines changed

.changeset/large-rats-hug.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hey-api/openapi-ts": patch
3+
---
4+
5+
**client-ofetch**: fix FormData boundary mismatch

examples/openapi-ts-ofetch/src/client/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/base-url-false/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/base-url-number/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/base-url-strict/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/base-url-string/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/clean-false/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/default/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-ofetch/import-file-extension-ts/client/client.gen.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export const createClient = (config: Config = {}): Client => {
125125
const applyRequestInterceptors = async (
126126
request: Request,
127127
opts: ResolvedRequestOptions,
128+
body: BodyInit | null | undefined,
128129
) => {
129130
for (const fn of interceptors.request.fns) {
130131
if (fn) {
@@ -138,6 +139,18 @@ export const createClient = (config: Config = {}): Client => {
138139
// body comes only from getValidRequestBody(options)
139140
// reflect signal if present
140141
opts.signal = (request as any).signal as AbortSignal | undefined;
142+
143+
// When body is FormData, remove Content-Type header to avoid boundary mismatch.
144+
// Note: We already delete Content-Type in resolveOptions for FormData, but the
145+
// Request constructor (line 175) re-adds it with an auto-generated boundary.
146+
// Since we pass the original FormData (not the Request's body) to ofetch, and
147+
// ofetch will generate its own boundary, we must remove the Request's Content-Type
148+
// to let ofetch set the correct one. Otherwise the boundary in the header won't
149+
// match the boundary in the actual multipart body sent by ofetch.
150+
if (typeof FormData !== 'undefined' && body instanceof FormData) {
151+
opts.headers.delete('Content-Type');
152+
}
153+
141154
return request;
142155
};
143156

@@ -176,7 +189,7 @@ export const createClient = (config: Config = {}): Client => {
176189
};
177190
let request = new Request(url, requestInit);
178191

179-
request = await applyRequestInterceptors(request, opts);
192+
request = await applyRequestInterceptors(request, opts, networkBody);
180193
const finalUrl = request.url;
181194

182195
// build ofetch options and perform the request (.raw keeps the Response)
@@ -235,7 +248,7 @@ export const createClient = (config: Config = {}): Client => {
235248
method,
236249
onRequest: async (url, init) => {
237250
let request = new Request(url, init);
238-
request = await applyRequestInterceptors(request, opts);
251+
request = await applyRequestInterceptors(request, opts, networkBody);
239252
return request;
240253
},
241254
serializedBody: networkBody as BodyInit | null | undefined,

0 commit comments

Comments
 (0)