Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/fix-rest-client-delete-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/api-client': patch
---

Fixes `RestClient.delete` to send `params` as a request body, matching `post`/`put`. Previously the body was silently dropped, breaking DELETE endpoints that expect a body.
80 changes: 80 additions & 0 deletions packages/api-client/__tests__/requests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import fetchMock from 'jest-fetch-mock';

import { RestClient } from '../src/index';

beforeAll(() => {
fetchMock.enableMocks();
});

afterAll(() => {
fetchMock.disableMocks();
});

beforeEach(() => {
fetchMock.resetMocks();
fetchMock.doMock();
fetchMock.mockResponse(JSON.stringify({ status: 'success' }));
});

const lastCall = () => {
const { calls } = fetchMock.mock;
expect(calls.length).toBeGreaterThan(0);
return calls[calls.length - 1];
};

const methodOf = (call: any) => (call[1] as RequestInit).method;
const bodyOf = (call: any) => (call[1] as RequestInit).body;
const headerOf = (call: any, name: string) => (call[1] as RequestInit).headers?.[name as keyof HeadersInit];

test('DELETE with object params serializes JSON body and sets Content-Type', async () => {
const client = new RestClient({ baseUrl: 'https://example.com' });

const result = await client.delete('/v1/rooms.cleanHistory', { roomId: 'abc', latest: '2020-01-01' });

expect(result).toMatchObject({ status: 'success' });
const call = lastCall();
expect(methodOf(call)).toBe('DELETE');
expect(JSON.parse(bodyOf(call) as string)).toEqual({ roomId: 'abc', latest: '2020-01-01' });
expect(headerOf(call, 'Content-Type')).toBe('application/json');
});

test('DELETE without params sends no body and no Content-Type', async () => {
const client = new RestClient({ baseUrl: 'https://example.com' });

await client.delete('/v1/rooms.cleanHistory');

const call = lastCall();
expect(methodOf(call)).toBe('DELETE');
expect(bodyOf(call)).toBeUndefined();
expect(headerOf(call, 'Content-Type')).toBeUndefined();
});

test('DELETE with a File forwards a multipart body including the file and fields', async () => {
const client = new RestClient({ baseUrl: 'https://example.com' });
const file = new File(['content'], 'note.txt', { type: 'text/plain' });

await client.delete('/v1/rooms.cleanHistory', { roomId: 'abc', file });

const call = lastCall();
expect(methodOf(call)).toBe('DELETE');
const body = bodyOf(call);
expect(body).toBeInstanceOf(FormData);
const entries = Array.from((body as FormData).entries());
expect(entries).toEqual(
expect.arrayContaining([
['roomId', 'abc'],
['file', file],
]),
);
});

test('DELETE returning 204 resolves to an empty object', async () => {
fetchMock.resetMocks();
fetchMock.doMock();
fetchMock.mockResponse('', { status: 204 });
const client = new RestClient({ baseUrl: 'https://example.com' });

const result = await client.delete('/v1/rooms.cleanHistory');

expect(result).toEqual({});
});
24 changes: 20 additions & 4 deletions packages/api-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function buildFormData(data?: Record<string, any> | void, formData = new FormDat

if (typeof data === 'object' && !(data instanceof File)) {
Object.keys(data).forEach((key) => {
buildFormData(formData, data[key], parentKey ? `${parentKey}[${key}]` : key);
buildFormData(data[key], formData, parentKey ? `${parentKey}[${key}]` : key);
});
} else {
data && parentKey && formData.append(parentKey, data);
Expand Down Expand Up @@ -188,10 +188,26 @@ export class RestClient implements RestClientInterface {

async delete<TPathPattern extends MatchPathPattern<TPath>, TPath extends PathFor<'DELETE'>>(
endpoint: TPath,
_params?: ParamsFor<'DELETE', TPathPattern>,
options: Omit<RequestInit, 'method'> = {},
params?: ParamsFor<'DELETE', TPathPattern>,
{ headers, ...options }: Omit<RequestInit, 'method'> = {},
): Promise<Serialized<OperationResult<'DELETE', TPathPattern>>> {
const response = await this.send(endpoint, 'DELETE', options ?? {});
const isFormData = checkIfIsFormData(params);
const response = await this.send(endpoint, 'DELETE', {
...(params !== undefined && { body: isFormData ? buildFormData(params) : JSON.stringify(params) }),
Comment thread
Rohit3523 marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

headers: {
Accept: 'application/json',
...(!isFormData && params !== undefined && { 'Content-Type': 'application/json' }),
...headers,
},

...options,
});

if (response.status === 204) {
return {} as any;
}

return response.json();
}
Comment thread
Rohit3523 marked this conversation as resolved.

Expand Down
Loading