Skip to content

Commit 33519f1

Browse files
Fix multipart form submission (#26)
1 parent 340f8aa commit 33519f1

7 files changed

+37
-44
lines changed

package-lock.json

-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
"reflect-metadata": "^0.1.13"
4949
},
5050
"devDependencies": {
51-
"@golevelup/ts-jest": "^0.3.4",
5251
"@types/jest": "^29.2.5",
5352
"@typescript-eslint/eslint-plugin": "^5.49.0",
5453
"@typescript-eslint/parser": "^5.49.0",

src/serialize/default-serializer.spec.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { createMock } from '@golevelup/ts-jest';
2-
31
import { nameField } from '../../test/stubs';
42
import { defaultSerializer } from './default-serializer';
53
import { fieldsToNameValuePairs } from './fields-to-name-value-pairs';
@@ -33,12 +31,12 @@ describe('defaultSerializer', () => {
3331

3432
it('should serialize application/x-www-form-urlencoded', async () => {
3533
const type = 'application/x-www-form-urlencoded';
36-
const params = createMock<URLSearchParams>();
37-
mockedSerializeUrlEncodedForm.mockReturnValueOnce(params);
34+
const content = new URLSearchParams();
35+
mockedSerializeUrlEncodedForm.mockReturnValueOnce(content);
3836

3937
const result = await defaultSerializer(type, fields);
4038

41-
expect(result).toStrictEqual({ contentType: type, content: params });
39+
expect(result).toEqual({ content: content, contentType: type });
4240
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledTimes(1);
4341
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledWith(fields);
4442
expect(mockedSerializeUrlEncodedForm).toHaveBeenCalledTimes(1);
@@ -47,12 +45,12 @@ describe('defaultSerializer', () => {
4745

4846
it('should serialize multipart/form-data', async () => {
4947
const type = 'multipart/form-data';
50-
const content = 'foo';
48+
const content = new FormData();
5149
mockedSerializeMultipartFormData.mockReturnValueOnce(content);
5250

5351
const result = await defaultSerializer(type, fields);
5452

55-
expect(result).toEqual({ contentType: type, content });
53+
expect(result).toEqual({ content, contentType: undefined });
5654
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledTimes(1);
5755
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledWith(fields);
5856
expect(mockedSerializeMultipartFormData).toHaveBeenCalledTimes(1);
@@ -66,7 +64,7 @@ describe('defaultSerializer', () => {
6664

6765
const result = await defaultSerializer(type, fields);
6866

69-
expect(result).toEqual({ contentType: type, content });
67+
expect(result).toEqual({ content, contentType: type });
7068
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledTimes(1);
7169
expect(mockedFieldsToNamedValuePairs).toHaveBeenCalledWith(fields);
7270
expect(mockedSerializePlainText).toHaveBeenCalledTimes(1);

src/serialize/default-serializer.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ const typeToSerializeFn = new Map<string, SerializeFn>([
2323
export const defaultSerializer: Serializer = (type: string, fields: Field[]): Promise<Serialization> =>
2424
new Promise((resolve, reject) => {
2525
const serialize = typeToSerializeFn.get(type);
26-
return serialize == null
27-
? reject(new UnsupportedSerializerTypeError(type))
28-
: resolve({
29-
contentType: type,
30-
content: serialize(fieldsToNameValuePairs(fields))
31-
});
26+
if (serialize == null) return reject(new UnsupportedSerializerTypeError(type));
27+
28+
const serialization = serialize(fieldsToNameValuePairs(fields));
29+
30+
return resolve({
31+
content: serialization,
32+
// let the Fetch API generate a boundary parameter for FormData
33+
contentType: serialization instanceof FormData ? undefined : type
34+
});
3235
});

src/serialize/serialization.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
/**
2-
* Result of serializing and {@link Action}
2+
* Result of serializing an {@link Action}
33
*/
44
export interface Serialization {
5-
contentType: string;
65
content: BodyInit;
6+
7+
/**
8+
* Media type of `content`. Omit this value to let the Fetch API generate the
9+
* `Content-Type` header.
10+
*/
11+
contentType?: string;
712
}

src/submit.spec.ts

+12-19
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe('submit', () => {
88
const baseUrl = 'https://api.example.com';
99
const path = '/foo';
1010
const url = new URL(path, baseUrl).toString();
11-
const responseBody = 'Success!';
1211

1312
beforeEach(() => {
1413
if (!nock.isActive()) {
@@ -38,13 +37,12 @@ describe('submit', () => {
3837
)}`;
3938

4039
it('should make HTTP GET request when method is missing', async () => {
41-
const scope = nock(baseUrl).get(path).reply(200, responseBody);
40+
const scope = nock(baseUrl).get(path).reply(204);
4241

4342
const response = await submit(action);
4443

4544
expect(response.url).toBe(url);
46-
expect(response.status).toBe(200);
47-
await expect(response.text()).resolves.toBe(responseBody);
45+
expect(response.status).toBe(204);
4846
expect(scope.isDone()).toBe(true);
4947
});
5048

@@ -54,13 +52,12 @@ describe('submit', () => {
5452
action.href = url;
5553
action.method = method;
5654
action.fields = [nameField, emailField];
57-
const scope = nock(baseUrl).intercept(`${path}?${urlEncodedForm}`, method).reply(200, responseBody);
55+
const scope = nock(baseUrl).intercept(`${path}?${urlEncodedForm}`, method).reply(204);
5856

5957
const response = await submit(action);
6058

6159
expect(response.url).toBe(`${url}?${urlEncodedForm}`);
62-
expect(response.status).toBe(200);
63-
await expect(response.text()).resolves.toBe(responseBody);
60+
expect(response.status).toBe(204);
6461
expect(scope.isDone()).toBe(true);
6562
});
6663

@@ -70,13 +67,12 @@ describe('submit', () => {
7067
action.href = url;
7168
action.method = method;
7269
action.fields = [nameField, emailField];
73-
const scope = nock(baseUrl).intercept(path, method, urlEncodedForm).reply(200, responseBody);
70+
const scope = nock(baseUrl).intercept(path, method, urlEncodedForm).reply(204);
7471

7572
const response = await submit(action);
7673

7774
expect(response.url).toBe(url);
78-
expect(response.status).toBe(200);
79-
await expect(response.text()).resolves.toBe(responseBody);
75+
expect(response.status).toBe(204);
8076
expect(scope.isDone()).toBe(true);
8177
});
8278

@@ -96,40 +92,37 @@ describe('submit', () => {
9692
const serializer: Serializer = () => Promise.resolve({ content, contentType });
9793
const scope = nock(baseUrl, { reqheaders: { 'Content-Type': contentType } })
9894
.post(path, content)
99-
.reply(200, responseBody);
95+
.reply(204);
10096

10197
const response = await submit(action, { serializer });
10298

10399
expect(response.url).toBe(url);
104-
expect(response.status).toBe(200);
105-
await expect(response.text()).resolves.toBe(responseBody);
100+
expect(response.status).toBe(204);
106101
expect(scope.isDone()).toBe(true);
107102
});
108103

109104
it('should accept and send request options', async () => {
110105
const apiKey = 'foo-bar-baz';
111106
const headers = { 'Api-Key': apiKey };
112-
const scope = nock(baseUrl, { reqheaders: headers }).get(path).reply(200, responseBody);
107+
const scope = nock(baseUrl, { reqheaders: headers }).get(path).reply(204);
113108

114109
const response = await submit(action, { requestInit: { headers } });
115110

116111
expect(response.url).toBe(url);
117-
expect(response.status).toBe(200);
118-
await expect(response.text()).resolves.toBe(responseBody);
112+
expect(response.status).toBe(204);
119113
expect(scope.isDone()).toBe(true);
120114
});
121115

122116
it('should resolve relative URL', async () => {
123117
const action = new Action();
124118
action.name = 'do-something';
125119
action.href = path;
126-
const scope = nock(baseUrl).get(path).reply(200, responseBody);
120+
const scope = nock(baseUrl).get(path).reply(204);
127121

128122
const response = await submit(action, { baseUrl });
129123

130124
expect(response.url).toBe(url);
131-
expect(response.status).toBe(200);
132-
await expect(response.text()).resolves.toBe(responseBody);
125+
expect(response.status).toBe(204);
133126
expect(scope.isDone()).toBe(true);
134127
});
135128
});

src/submit.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export async function submit(action: Action, options: SubmitOptions = {}): Promi
5151
if (init.method === 'GET' || init.method === 'DELETE') {
5252
target.search = serialization.content.toString();
5353
} else {
54-
init.headers = { ...init.headers, 'Content-Type': serialization.contentType };
54+
if (serialization.contentType) {
55+
init.headers = { ...init.headers, 'Content-Type': serialization.contentType };
56+
}
5557
init.body = serialization.content;
5658
}
5759
}

0 commit comments

Comments
 (0)