@@ -404,5 +404,118 @@ describe('request interceptor', () => {
404404 ) ;
405405} ) ;
406406
407+ describe ( 'FormData boundary handling' , ( ) => {
408+ const client = createClient ( { baseUrl : 'https://example.com' } ) ;
409+
410+ it ( 'should not include Content-Type header for FormData body to avoid boundary mismatch' , async ( ) => {
411+ const mockResponse = new Response ( JSON . stringify ( { success : true } ) , {
412+ headers : {
413+ 'Content-Type' : 'application/json' ,
414+ } ,
415+ status : 200 ,
416+ } ) ;
417+
418+ const mockOfetch = makeMockOfetch ( mockResponse ) ;
419+
420+ const formData = new FormData ( ) ;
421+ formData . append ( 'field1' , 'value1' ) ;
422+ formData . append ( 'field2' , 'value2' ) ;
423+
424+ await client . post ( {
425+ body : formData ,
426+ bodySerializer : null ,
427+ ofetch : mockOfetch as any ,
428+ url : '/upload' ,
429+ } ) ;
430+
431+ // Verify that ofetch.raw was called
432+ expect ( mockOfetch . raw ) . toHaveBeenCalledOnce ( ) ;
433+
434+ // Get the options passed to ofetch.raw
435+ const call = ( mockOfetch . raw as any ) . mock . calls [ 0 ] ;
436+ const opts = call [ 1 ] ;
437+
438+ // Verify that FormData is passed as body
439+ expect ( opts . body ) . toBeInstanceOf ( FormData ) ;
440+
441+ // Verify that Content-Type header is NOT set (so ofetch can set its own boundary)
442+ expect ( opts . headers . get ( 'Content-Type' ) ) . toBeNull ( ) ;
443+ } ) ;
444+
445+ it ( 'should preserve Content-Type header for non-FormData bodies' , async ( ) => {
446+ const mockResponse = new Response ( JSON . stringify ( { success : true } ) , {
447+ headers : {
448+ 'Content-Type' : 'application/json' ,
449+ } ,
450+ status : 200 ,
451+ } ) ;
452+
453+ const mockOfetch = makeMockOfetch ( mockResponse ) ;
454+
455+ await client . post ( {
456+ body : { test : 'data' } ,
457+ ofetch : mockOfetch as any ,
458+ url : '/api' ,
459+ } ) ;
460+
461+ // Verify that ofetch.raw was called
462+ expect ( mockOfetch . raw ) . toHaveBeenCalledOnce ( ) ;
463+
464+ // Get the options passed to ofetch.raw
465+ const call = ( mockOfetch . raw as any ) . mock . calls [ 0 ] ;
466+ const opts = call [ 1 ] ;
467+
468+ // Verify that Content-Type header IS set for JSON
469+ expect ( opts . headers . get ( 'Content-Type' ) ) . toBe ( 'application/json' ) ;
470+ } ) ;
471+
472+ it ( 'should handle FormData with interceptors correctly' , async ( ) => {
473+ const mockResponse = new Response ( JSON . stringify ( { success : true } ) , {
474+ headers : {
475+ 'Content-Type' : 'application/json' ,
476+ } ,
477+ status : 200 ,
478+ } ) ;
479+
480+ const mockOfetch = makeMockOfetch ( mockResponse ) ;
481+
482+ const formData = new FormData ( ) ;
483+ formData . append ( 'field1' , 'value1' ) ;
484+
485+ const mockRequestInterceptor = vi
486+ . fn ( )
487+ . mockImplementation ( ( request : Request ) => {
488+ // Interceptor can modify headers but we should still remove Content-Type for FormData
489+ request . headers . set ( 'X-Custom-Header' , 'custom-value' ) ;
490+ return request ;
491+ } ) ;
492+
493+ const interceptorId = client . interceptors . request . use (
494+ mockRequestInterceptor ,
495+ ) ;
496+
497+ await client . post ( {
498+ body : formData ,
499+ bodySerializer : null ,
500+ ofetch : mockOfetch as any ,
501+ url : '/upload' ,
502+ } ) ;
503+
504+ expect ( mockRequestInterceptor ) . toHaveBeenCalledOnce ( ) ;
505+
506+ // Get the options passed to ofetch.raw
507+ const call = ( mockOfetch . raw as any ) . mock . calls [ 0 ] ;
508+ const opts = call [ 1 ] ;
509+
510+ // Verify that Content-Type is NOT set even after interceptor
511+ expect ( opts . headers . get ( 'Content-Type' ) ) . toBeNull ( ) ;
512+
513+ // Verify that custom header from interceptor IS preserved
514+ expect ( opts . headers . get ( 'X-Custom-Header' ) ) . toBe ( 'custom-value' ) ;
515+
516+ client . interceptors . request . eject ( interceptorId ) ;
517+ } ) ;
518+ } ) ;
519+
407520// Note: дополнительные проверки поведения ofetch (responseType/responseStyle/retry)
408521// не дублируем, чтобы набор тестов оставался сопоставим с другими клиентами.
0 commit comments