@@ -28,7 +28,7 @@ import {
28
28
} from './utils.gen' ;
29
29
30
30
type ReqInit = Omit < RequestInit , 'body' | 'headers' > & {
31
- body ?: any ;
31
+ body ?: BodyInit | null | undefined ;
32
32
headers : ReturnType < typeof mergeHeaders > ;
33
33
} ;
34
34
@@ -49,7 +49,8 @@ export const createClient = (config: Config = {}): Client => {
49
49
ResolvedRequestOptions
50
50
> ( ) ;
51
51
52
- const beforeRequest = async ( options : RequestOptions ) => {
52
+ // precompute serialized / network body
53
+ const resolveOptions = async ( options : RequestOptions ) => {
53
54
const opts = {
54
55
..._config ,
55
56
...options ,
@@ -72,12 +73,44 @@ export const createClient = (config: Config = {}): Client => {
72
73
opts . serializedBody = opts . bodySerializer ( opts . body ) ;
73
74
}
74
75
75
- // remove Content-Type header if body is empty to avoid sending invalid requests
76
+ // remove Content-Type if body is empty to avoid invalid requests
76
77
if ( opts . body === undefined || opts . serializedBody === '' ) {
77
78
opts . headers . delete ( 'Content-Type' ) ;
78
79
}
79
80
80
- // Precompute network body for retries and consistent handling
81
+ // if a raw body is provided (no serializer), adjust Content-Type only when it
82
+ // equals the default JSON value to better match the concrete body type
83
+ if (
84
+ opts . body !== undefined &&
85
+ opts . bodySerializer === null &&
86
+ ( opts . headers . get ( 'Content-Type' ) || '' ) . toLowerCase ( ) ===
87
+ 'application/json'
88
+ ) {
89
+ const b : unknown = opts . body ;
90
+ if ( typeof FormData !== 'undefined' && b instanceof FormData ) {
91
+ // let the runtime set the multipart boundary
92
+ opts . headers . delete ( 'Content-Type' ) ;
93
+ } else if (
94
+ typeof URLSearchParams !== 'undefined' &&
95
+ b instanceof URLSearchParams
96
+ ) {
97
+ // standard urlencoded content type (+ charset)
98
+ opts . headers . set (
99
+ 'Content-Type' ,
100
+ 'application/x-www-form-urlencoded;charset=UTF-8' ,
101
+ ) ;
102
+ } else if ( typeof Blob !== 'undefined' && b instanceof Blob ) {
103
+ const t = b . type ?. trim ( ) ;
104
+ if ( t ) {
105
+ opts . headers . set ( 'Content-Type' , t ) ;
106
+ } else {
107
+ // unknown blob type: avoid sending a misleading JSON header
108
+ opts . headers . delete ( 'Content-Type' ) ;
109
+ }
110
+ }
111
+ }
112
+
113
+ // precompute network body (stability for retries and interceptors)
81
114
const networkBody = getValidRequestBody ( opts ) as
82
115
| RequestInit [ 'body' ]
83
116
| null
@@ -88,20 +121,52 @@ export const createClient = (config: Config = {}): Client => {
88
121
return { networkBody, opts, url } ;
89
122
} ;
90
123
124
+ // apply request interceptors and mirror header/method/signal back to opts
125
+ const applyRequestInterceptors = async (
126
+ request : Request ,
127
+ opts : ResolvedRequestOptions ,
128
+ ) => {
129
+ for ( const fn of interceptors . request . fns ) {
130
+ if ( fn ) {
131
+ request = await fn ( request , opts ) ;
132
+ }
133
+ }
134
+ // reflect interceptor changes into opts used by the network layer
135
+ opts . headers = request . headers ;
136
+ opts . method = request . method as Uppercase < HttpMethod > ;
137
+ // ignore request.body changes to avoid turning serialized bodies into streams
138
+ // body comes only from getValidRequestBody(options)
139
+ // reflect signal if present
140
+ opts . signal = ( request as any ) . signal as AbortSignal | undefined ;
141
+ return request ;
142
+ } ;
143
+
144
+ // build ofetch options with stable retry logic based on body repeatability
145
+ const buildNetworkOptions = (
146
+ opts : ResolvedRequestOptions ,
147
+ body : BodyInit | null | undefined ,
148
+ responseType : OfetchResponseType | undefined ,
149
+ ) => {
150
+ const effectiveRetry = isRepeatableBody ( body )
151
+ ? ( opts . retry as any )
152
+ : ( 0 as any ) ;
153
+ return buildOfetchOptions ( opts , body , responseType , effectiveRetry ) ;
154
+ } ;
155
+
91
156
const request : Client [ 'request' ] = async ( options ) => {
92
157
const {
93
158
networkBody : initialNetworkBody ,
94
159
opts,
95
160
url,
96
- } = await beforeRequest ( options as any ) ;
97
- // Compute response type mapping once
161
+ } = await resolveOptions ( options as any ) ;
162
+ // map parseAs -> ofetch responseType once per request
98
163
const ofetchResponseType : OfetchResponseType | undefined =
99
164
mapParseAsToResponseType ( opts . parseAs , opts . responseType ) ;
100
165
101
166
const $ofetch = opts . ofetch ?? ofetch ;
102
167
103
- // Always create Request pre- network (align with client-fetch)
104
- let networkBody = initialNetworkBody ;
168
+ // create Request before network to run middleware consistently
169
+ const networkBody = initialNetworkBody ;
105
170
const requestInit : ReqInit = {
106
171
body : networkBody ,
107
172
headers : opts . headers as Headers ,
@@ -111,37 +176,14 @@ export const createClient = (config: Config = {}): Client => {
111
176
} ;
112
177
let request = new Request ( url , requestInit ) ;
113
178
114
- for ( const fn of interceptors . request . fns ) {
115
- if ( fn ) {
116
- request = await fn ( request , opts ) ;
117
- }
118
- }
119
-
120
- // Reflect any interceptor changes into opts used for network and downstream
121
- opts . headers = request . headers ;
122
- opts . method = request . method as Uppercase < HttpMethod > ;
123
- // Attempt to reflect possible signal/body changes (safely)
124
-
125
- const reqBody = ( request as any ) . body as unknown ;
126
- let effectiveRetry = opts . retry ;
127
- if ( reqBody !== undefined && reqBody !== null ) {
128
- if ( isRepeatableBody ( reqBody ) ) {
129
- networkBody = reqBody as BodyInit ;
130
- } else {
131
- networkBody = reqBody as BodyInit ;
132
- // Disable retries for non-repeatable bodies
133
- effectiveRetry = 0 as any ;
134
- }
135
- }
136
-
137
- opts . signal = ( request as any ) . signal as AbortSignal | undefined ;
179
+ request = await applyRequestInterceptors ( request , opts ) ;
138
180
const finalUrl = request . url ;
139
181
140
- // Build ofetch options and perform the request
141
- const responseOptions = buildOfetchOptions (
182
+ // build ofetch options and perform the request (.raw keeps the Response)
183
+ const responseOptions = buildNetworkOptions (
142
184
opts as ResolvedRequestOptions ,
143
- networkBody ?? undefined ,
144
- effectiveRetry as any ,
185
+ networkBody ,
186
+ ofetchResponseType ,
145
187
) ;
146
188
147
189
let response = await $ofetch . raw ( finalUrl , responseOptions ) ;
@@ -167,7 +209,7 @@ export const createClient = (config: Config = {}): Client => {
167
209
}
168
210
}
169
211
170
- // Ensure error is never undefined after interceptors
212
+ // ensure error is never undefined after interceptors
171
213
finalError = ( finalError as any ) || ( { } as string ) ;
172
214
173
215
if ( opts . throwOnError ) {
@@ -183,21 +225,17 @@ export const createClient = (config: Config = {}): Client => {
183
225
184
226
const makeSseFn =
185
227
( method : Uppercase < HttpMethod > ) => async ( options : RequestOptions ) => {
186
- const { networkBody, opts, url } = await beforeRequest ( options ) ;
228
+ const { networkBody, opts, url } = await resolveOptions ( options ) ;
187
229
const optsForSse : any = { ...opts } ;
188
- delete optsForSse . body ;
230
+ delete optsForSse . body ; // body is provided via serializedBody below
189
231
return createSseClient ( {
190
232
...optsForSse ,
191
233
fetch : opts . fetch ,
192
234
headers : opts . headers as Headers ,
193
235
method,
194
236
onRequest : async ( url , init ) => {
195
237
let request = new Request ( url , init ) ;
196
- for ( const fn of interceptors . request . fns ) {
197
- if ( fn ) {
198
- request = await fn ( request , opts ) ;
199
- }
200
- }
238
+ request = await applyRequestInterceptors ( request , opts ) ;
201
239
return request ;
202
240
} ,
203
241
serializedBody : networkBody as BodyInit | null | undefined ,
0 commit comments