@@ -4,14 +4,15 @@ import createClient, {
44 type BodySerializer ,
55 type FetchOptions ,
66 type MethodResponse ,
7+ type HeadersOptions ,
78 type Middleware ,
89 type MiddlewareCallbackParams ,
910 type QuerySerializerOptions ,
1011 type Client ,
1112 type PathBasedClient ,
1213 createPathBasedClient ,
1314} from "../src/index.js" ;
14- import { server , baseUrl , useMockRequestHandler , toAbsoluteURL } from "./fixtures/mock-server.js" ;
15+ import { baseUrl , server , toAbsoluteURL , useMockRequestHandler } from "./fixtures/mock-server.js" ;
1516import type { paths } from "./fixtures/api.js" ;
1617
1718beforeAll ( ( ) => {
@@ -819,12 +820,7 @@ describe("client", () => {
819820 await client . GET ( "/self" ) ;
820821
821822 // assert default headers were passed
822- expect ( getRequest ( ) . headers ) . toEqual (
823- new Headers ( {
824- ...headers , // assert new header got passed
825- "Content-Type" : "application/json" , // probably doesn’t need to get tested, but this was simpler than writing lots of code to ignore these
826- } ) ,
827- ) ;
823+ expect ( getRequest ( ) . headers ) . toEqual ( new Headers ( headers ) ) ;
828824 } ) ;
829825
830826 it ( "can be overridden" , async ( ) => {
@@ -850,7 +846,6 @@ describe("client", () => {
850846 expect ( getRequest ( ) . headers ) . toEqual (
851847 new Headers ( {
852848 "Cache-Control" : "no-cache" ,
853- "Content-Type" : "application/json" ,
854849 } ) ,
855850 ) ;
856851 } ) ;
@@ -894,6 +889,139 @@ describe("client", () => {
894889 } ) ;
895890 } ) ;
896891
892+ describe ( "content-type" , ( ) => {
893+ const BODY_ACCEPTING_METHODS = [ [ "PUT" ] , [ "POST" ] , [ "DELETE" ] , [ "OPTIONS" ] , [ "PATCH" ] ] as const ;
894+ const ALL_METHODS = [ ...BODY_ACCEPTING_METHODS , [ "GET" ] , [ "HEAD" ] ] as const ;
895+
896+ const fireRequestAndGetContentType = async ( options : {
897+ defaultHeaders ?: HeadersOptions ;
898+ method : ( typeof ALL_METHODS ) [ number ] [ number ] ;
899+ fetchOptions : FetchOptions < any > ;
900+ } ) => {
901+ const client = createClient < any > ( { baseUrl, headers : options . defaultHeaders } ) ;
902+ const { getRequest } = useMockRequestHandler ( {
903+ baseUrl,
904+ method : "all" ,
905+ path : "/blogposts-optional" ,
906+ status : 200 ,
907+ } ) ;
908+ await client [ options . method ] ( "/blogposts-optional" , options . fetchOptions as any ) ;
909+
910+ const request = getRequest ( ) ;
911+ return request . headers . get ( "content-type" ) ;
912+ } ;
913+
914+ it . each ( ALL_METHODS ) ( "no content-type for body-less requests - %s" , async ( method ) => {
915+ const contentType = await fireRequestAndGetContentType ( {
916+ method,
917+ fetchOptions : { } ,
918+ } ) ;
919+
920+ expect ( contentType ) . toBe ( null ) ;
921+ } ) ;
922+
923+ it . each ( ALL_METHODS ) ( "no content-type for `undefined` body requests - %s" , async ( method ) => {
924+ const contentType = await fireRequestAndGetContentType ( {
925+ method,
926+ fetchOptions : {
927+ body : undefined ,
928+ } ,
929+ } ) ;
930+
931+ expect ( contentType ) . toBe ( null ) ;
932+ } ) ;
933+
934+ const BODIES = [ { prop : "a" } , { } , "" , "str" , null , false , 0 , 1 , new Date ( "2024-08-07T09:52:00.836Z" ) ] as const ;
935+ const METHOD_BODY_COMBINATIONS = BODY_ACCEPTING_METHODS . flatMap ( ( [ method ] ) =>
936+ BODIES . map ( ( body ) => [ method , body ] as const ) ,
937+ ) ;
938+
939+ it . each ( METHOD_BODY_COMBINATIONS ) (
940+ "implicit default content-type for body-full requests - %s, %j" ,
941+ async ( method , body ) => {
942+ const contentType = await fireRequestAndGetContentType ( {
943+ method,
944+ fetchOptions : {
945+ body,
946+ } ,
947+ } ) ;
948+
949+ expect ( contentType ) . toBe ( "application/json" ) ;
950+ } ,
951+ ) ;
952+
953+ it . each ( METHOD_BODY_COMBINATIONS ) (
954+ "provided default content-type for body-full requests - %s, %j" ,
955+ async ( method , body ) => {
956+ const contentType = await fireRequestAndGetContentType ( {
957+ defaultHeaders : {
958+ "content-type" : "application/my-json" ,
959+ } ,
960+ method,
961+ fetchOptions : {
962+ body,
963+ } ,
964+ } ) ;
965+
966+ expect ( contentType ) . toBe ( "application/my-json" ) ;
967+ } ,
968+ ) ;
969+
970+ it . each ( METHOD_BODY_COMBINATIONS ) (
971+ "native-fetch default content-type for body-full requests, when default is suppressed - %s, %j" ,
972+ async ( method , body ) => {
973+ const contentType = await fireRequestAndGetContentType ( {
974+ defaultHeaders : {
975+ "content-type" : null ,
976+ } ,
977+ method,
978+ fetchOptions : {
979+ body,
980+ } ,
981+ } ) ;
982+ // the fetch implementation won't allow sending a body without content-type,
983+ // and it defaults to `text/plain;charset=UTF-8`, however the actual default value
984+ // is irrelevant and might be flaky across different fetch implementations
985+ // for us, it's important that it's not `application/json`
986+ expect ( contentType ) . not . toBe ( "application/json" ) ;
987+ } ,
988+ ) ;
989+
990+ it . each ( METHOD_BODY_COMBINATIONS ) (
991+ "specified content-type for body-full requests - %s, %j" ,
992+ async ( method , body ) => {
993+ const contentType = await fireRequestAndGetContentType ( {
994+ method,
995+ fetchOptions : {
996+ body,
997+ headers : {
998+ "content-type" : "application/my-json" ,
999+ } ,
1000+ } ,
1001+ } ) ;
1002+
1003+ expect ( contentType ) . toBe ( "application/my-json" ) ;
1004+ } ,
1005+ ) ;
1006+
1007+ it . each ( METHOD_BODY_COMBINATIONS ) (
1008+ "specified content-type for body-full requests, even when default is suppressed - %s, %j" ,
1009+ async ( method , body ) => {
1010+ const contentType = await fireRequestAndGetContentType ( {
1011+ method,
1012+ fetchOptions : {
1013+ body,
1014+ headers : {
1015+ "content-type" : "application/my-json" ,
1016+ } ,
1017+ } ,
1018+ } ) ;
1019+
1020+ expect ( contentType ) . toBe ( "application/my-json" ) ;
1021+ } ,
1022+ ) ;
1023+ } ) ;
1024+
8971025 describe ( "fetch" , ( ) => {
8981026 it ( "createClient" , async ( ) => {
8991027 function createCustomFetch ( data : any ) {
0 commit comments