Skip to content

Commit 77db06b

Browse files
authored
fix: client not encoding application/x-www-form-urlencoded body correctly (#524)
1 parent 68043f0 commit 77db06b

4 files changed

Lines changed: 67 additions & 15 deletions

File tree

.changeset/tricky-chicken-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ts-rest/core': patch
3+
---
4+
5+
Fix client not correctly encoding application/x-www-form-urlencoded body

libs/ts-rest/core/src/lib/client.spec.ts

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,20 @@ const postsRouter = c.router({
6161
authorId: z.string(),
6262
}),
6363
},
64-
createPostXForm: {
64+
echoPostXForm: {
6565
method: 'POST',
66-
path: '/posts',
66+
path: '/echo',
67+
contentType: 'application/x-www-form-urlencoded',
68+
body: z.object({
69+
foo: z.string(),
70+
bar: z.string(),
71+
}),
6772
responses: {
68-
200: c.response<Post>(),
73+
200: c.otherResponse({
74+
contentType: 'text/plain',
75+
body: z.string(),
76+
}),
6977
},
70-
body: z.string(),
71-
contentType: 'application/x-www-form-urlencoded',
7278
},
7379
mutationWithQuery: {
7480
method: 'POST',
@@ -444,32 +450,65 @@ describe('client', () => {
444450
expect(result.headers.get('Content-Type')).toBe('application/json');
445451
});
446452

447-
it('w/ body and content-type header', async () => {
448-
const value = 'key=value';
453+
it('w/ urlencoded body - passing object', async () => {
449454
fetchMock.postOnce(
450455
{
451-
url: 'https://api.com/posts',
456+
url: 'https://api.com/echo',
452457
headers: {
453458
'content-type': 'application/x-www-form-urlencoded',
454459
},
455460
},
461+
(_, req) => {
462+
expect(req.body).toBeInstanceOf(URLSearchParams);
463+
464+
return {
465+
body: req.body!.toString(),
466+
status: 200,
467+
};
468+
},
469+
);
470+
471+
const result = await client.posts.echoPostXForm({
472+
body: {
473+
foo: 'foo',
474+
bar: 'bar',
475+
},
476+
});
477+
478+
expect(result.status).toBe(200);
479+
expect(result.headers.get('Content-Type')).toBe(
480+
'text/plain;charset=UTF-8',
481+
);
482+
expect(result.body).toBe('foo=foo&bar=bar');
483+
});
484+
485+
it('w/ urlencoded body - passing string', async () => {
486+
fetchMock.postOnce(
456487
{
457-
body: value,
458-
status: 200,
488+
url: 'https://api.com/echo',
459489
headers: {
460490
'content-type': 'application/x-www-form-urlencoded',
461491
},
462492
},
493+
(_, req) => {
494+
expect(typeof req.body).toBe('string');
495+
496+
return {
497+
body: req.body,
498+
status: 200,
499+
};
500+
},
463501
);
464502

465-
const result = await client.posts.createPostXForm({
466-
body: 'key=value',
503+
const result = await client.posts.echoPostXForm({
504+
body: 'foo=foo&bar=bar',
467505
});
468506

469507
expect(result.status).toBe(200);
470508
expect(result.headers.get('Content-Type')).toBe(
471-
'application/x-www-form-urlencoded',
509+
'text/plain;charset=UTF-8',
472510
);
511+
expect(result.body).toBe('foo=foo&bar=bar');
473512
});
474513

475514
it('w/ query params', async () => {

libs/ts-rest/core/src/lib/client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export type ApiFetcherArgs = {
5555
path: string;
5656
method: string;
5757
headers: Record<string, string>;
58-
body: FormData | string | null | undefined;
58+
body: FormData | URLSearchParams | string | null | undefined;
5959
rawBody: unknown;
6060
rawQuery: unknown;
6161
contentType: AppRouteMutation['contentType'];
@@ -229,13 +229,19 @@ export const fetchApi = ({
229229
...combinedHeaders,
230230
'content-type': 'application/x-www-form-urlencoded',
231231
};
232+
232233
return apiFetcher({
233234
route,
234235
path,
235236
method: route.method,
236237
credentials: clientArgs.credentials,
237238
headers,
238-
body: body instanceof FormData ? body : createFormData(body),
239+
body:
240+
typeof body === 'string'
241+
? body
242+
: new URLSearchParams(
243+
body as Record<string, string> | URLSearchParams,
244+
),
239245
rawBody: body,
240246
rawQuery: query,
241247
contentType: 'application/x-www-form-urlencoded',

libs/ts-rest/core/src/lib/infer-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ type ClientInferRequestBase<
208208
? never
209209
: T['contentType'] extends 'multipart/form-data'
210210
? FormData | ZodInputOrType<T['body']>
211+
: T['contentType'] extends 'application/x-www-form-urlencoded'
212+
? string | ZodInputOrType<T['body']>
211213
: ZodInputOrType<T['body']>
212214
: never;
213215
query: 'query' extends keyof T

0 commit comments

Comments
 (0)