Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';

import type { ResolvedRequestOptions } from '../bundle';
import { createClient } from '../bundle/client';

type MockFetch = ((...args: any[]) => any) & {
Expand Down Expand Up @@ -221,3 +222,94 @@ describe('zero-length body handling', () => {
expect((result.data as Blob).size).toBeGreaterThan(0);
});
});

describe('request body handling', () => {
const client = createClient({ baseUrl: 'https://example.com' });

const scenarios = [
{
body: 'test string',
bodySerializer: null,
contentType: 'text/plain',
expectedSerializedValue: undefined,
expectedValue: async (request: Request) => await request.text(),
},
{
body: { key: 'value' },
bodySerializer: (body: object) => JSON.stringify(body),
contentType: 'application/json',
expectedSerializedValue: '{"key":"value"}',
expectedValue: async (request: Request) => await request.json(),
},
];

it.each(scenarios)(
'sends $contentType body',
async ({ body, bodySerializer, contentType, expectedValue }) => {
const mockResponse = new Response(JSON.stringify({ success: true }), {
headers: {
'Content-Type': 'application/json',
},
status: 200,
});

const mockFetch: MockFetch = vi.fn().mockResolvedValueOnce(mockResponse);

const result = await client.post({
body,
bodySerializer,
fetch: mockFetch,
headers: {
'Content-Type': contentType,
},
url: '/test',
});

await expect(expectedValue(result.request)).resolves.toEqual(body);
expect(result.request.headers.get('Content-Type')).toContain(contentType);
},
);

it.each(scenarios)(
'exposes $contentType serialized and raw body in interceptor',
async ({ body, bodySerializer, contentType, expectedSerializedValue }) => {
const mockResponse = new Response(JSON.stringify({ success: true }), {
headers: {
'Content-Type': 'application/json',
},
status: 200,
});

const mockFetch: MockFetch = vi.fn().mockResolvedValueOnce(mockResponse);

const mockRequestInterceptor = vi
.fn()
.mockImplementation(
(request: Request, options: ResolvedRequestOptions) => {
expect(options.serializedBody).toBe(expectedSerializedValue);
expect(options.body).toBe(body);

return request;
},
);

const interceptorId = client.interceptors.request.use(
mockRequestInterceptor,
);

await client.post({
body,
bodySerializer,
fetch: mockFetch,
headers: {
'Content-Type': contentType,
},
url: '/test',
});

expect(mockRequestInterceptor).toHaveBeenCalledOnce();

client.interceptors.request.eject(interceptorId);
},
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const createClient = (config: Config = {}): Client => {
}

// remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.serializedBody === undefined || opts.serializedBody === '') {
if (opts.body === undefined || opts.body === '') {
opts.headers.delete('Content-Type');
}

Expand All @@ -78,9 +78,12 @@ export const createClient = (config: Config = {}): Client => {
const requestInit: ReqInit = {
redirect: 'follow',
...opts,
body: opts.serializedBody,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't all that you want something like this?

body: opts.serializedBody !== undefined ? opts.serializedBody : opts.body,

Copy link
Author

@franworks franworks Aug 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought about going the opts.serializedBody ?? opts.body route but decided to put it in a separate block for readability and clarity.

Body is already a property on the options object, no need to define or reassign if its not necessary. It was confusing for me to see body redefined in the client-next plugin like body: options.body. I had to ask myself if something was different - it wasn't.

Happy to change it if you prefer the one liner.

};

if (opts.serializedBody) {
requestInit.body = opts.serializedBody;
}

let request = new Request(url, requestInit);

for (const fn of interceptors.request._fns) {
Expand Down
Loading