Skip to content

Commit 63eaec9

Browse files
committed
fix: tighten openapi-fetch excess property checks
1 parent 290255d commit 63eaec9

9 files changed

Lines changed: 152 additions & 11 deletions

File tree

.changeset/tough-coins-listen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
fix(openapi-fetch): reject excess request body and params properties

packages/openapi-fetch/src/index.d.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export type RequestOptions<T> = ParamsOption<T> &
129129
pathSerializer?: PathSerializer;
130130
parseAs?: ParseAs;
131131
fetch?: ClientOptions["fetch"];
132+
Request?: typeof Request;
132133
headers?: HeadersOptions;
133134
middleware?: Middleware[];
134135
};
@@ -195,6 +196,8 @@ export type MaybeOptionalInit<Params, Location extends keyof Params> =
195196
? FetchOptions<FilterKeys<Params, Location>> | undefined
196197
: FetchOptions<FilterKeys<Params, Location>>;
197198

199+
type InitWithParseAs<Init, P extends ParseAs | undefined> = Init & (P extends ParseAs ? { parseAs: P } : {});
200+
198201
// The final init param to accept.
199202
// - Determines if the param is optional or not.
200203
// - Performs arbitrary [key: string] addition.
@@ -206,25 +209,25 @@ export type ClientMethod<
206209
Paths extends Record<string, Record<HttpMethod, {}>>,
207210
Method extends HttpMethod,
208211
Media extends MediaType,
209-
> = <Path extends PathsWithMethod<Paths, Method>, Init extends MaybeOptionalInit<Paths[Path], Method>>(
212+
> = <Path extends PathsWithMethod<Paths, Method>, P extends ParseAs | undefined = undefined>(
210213
url: Path,
211-
...init: InitParam<Init>
212-
) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;
214+
...init: InitParam<InitWithParseAs<MaybeOptionalInit<Paths[Path], Method>, P>>
215+
) => Promise<FetchResponse<Paths[Path][Method], InitWithParseAs<MaybeOptionalInit<Paths[Path], Method>, P>, Media>>;
213216

214217
export type ClientRequestMethod<Paths extends Record<string, Record<HttpMethod, {}>>, Media extends MediaType> = <
215218
Method extends HttpMethod,
216219
Path extends PathsWithMethod<Paths, Method>,
217-
Init extends MaybeOptionalInit<Paths[Path], Method>,
220+
P extends ParseAs | undefined = undefined,
218221
>(
219222
method: Method,
220223
url: Path,
221-
...init: InitParam<Init>
222-
) => Promise<FetchResponse<Paths[Path][Method], Init, Media>>;
224+
...init: InitParam<InitWithParseAs<MaybeOptionalInit<Paths[Path], Method>, P>>
225+
) => Promise<FetchResponse<Paths[Path][Method], InitWithParseAs<MaybeOptionalInit<Paths[Path], Method>, P>, Media>>;
223226

224227
export type ClientForPath<PathInfo extends Record<string | number, any>, Media extends MediaType> = {
225-
[Method in keyof PathInfo as Uppercase<string & Method>]: <Init extends MaybeOptionalInit<PathInfo, Method>>(
226-
...init: InitParam<Init>
227-
) => Promise<FetchResponse<PathInfo[Method], Init, Media>>;
228+
[Method in keyof PathInfo as Uppercase<string & Method>]: <P extends ParseAs | undefined = undefined>(
229+
...init: InitParam<InitWithParseAs<MaybeOptionalInit<PathInfo, Method>, P>>
230+
) => Promise<FetchResponse<PathInfo[Method], InitWithParseAs<MaybeOptionalInit<PathInfo, Method>, P>, Media>>;
228231
};
229232

230233
export interface Client<Paths extends {}, Media extends MediaType = MediaType> {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { test } from "vitest";
2+
import createClient from "../../src/index.js";
3+
import type { paths } from "./schemas/common.js";
4+
5+
test("GET rejects undefined query params", () => {
6+
const client = createClient<paths>();
7+
8+
client.GET("/query-params", {
9+
params: {
10+
query: {
11+
array: [],
12+
// Regression test: extra query params should be rejected.
13+
// @ts-expect-error
14+
undefined_property: [],
15+
},
16+
},
17+
});
18+
});
19+
20+
test("client.request rejects undefined query params", () => {
21+
const client = createClient<paths>();
22+
23+
client.request("get", "/query-params", {
24+
params: {
25+
query: {
26+
array: [],
27+
// Regression test: extra query params should be rejected via client.request().
28+
// @ts-expect-error
29+
undefined_property: [],
30+
},
31+
},
32+
});
33+
});
34+
35+
test("GET rejects undefined path params", () => {
36+
const client = createClient<paths>();
37+
38+
client.GET("/resources/{id}", {
39+
params: {
40+
path: {
41+
id: 123,
42+
// Regression test: extra path params should be rejected.
43+
// @ts-expect-error
44+
undefined_property: 456,
45+
},
46+
},
47+
});
48+
});
49+
50+
test("client.request rejects undefined path params", () => {
51+
const client = createClient<paths>();
52+
53+
client.request("get", "/resources/{id}", {
54+
params: {
55+
path: {
56+
id: 123,
57+
// Regression test: extra path params should be rejected via client.request().
58+
// @ts-expect-error
59+
undefined_property: 456,
60+
},
61+
},
62+
});
63+
});

packages/openapi-fetch/test/common/schemas/common.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ export interface paths {
672672
number?: number;
673673
boolean?: boolean;
674674
array?: string[];
675+
second_array?: string[];
676+
third_array?: string[];
675677
object?: {
676678
foo: string;
677679
bar: string;
@@ -688,6 +690,8 @@ export interface paths {
688690
number?: number;
689691
boolean?: boolean;
690692
array?: string[];
693+
second_array?: string[];
694+
third_array?: string[];
691695
object?: {
692696
foo: string;
693697
bar: string;

packages/openapi-fetch/test/common/schemas/common.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,18 @@ paths:
392392
type: array
393393
items:
394394
type: string
395+
- in: query
396+
name: second_array
397+
schema:
398+
type: array
399+
items:
400+
type: string
401+
- in: query
402+
name: third_array
403+
schema:
404+
type: array
405+
items:
406+
type: string
395407
- in: query
396408
name: object
397409
schema:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test } from "vitest";
2+
import createClient from "../../src/index.js";
3+
import type { paths } from "./schemas/post.js";
4+
5+
test("POST rejects undefined request body properties", () => {
6+
const client = createClient<paths>();
7+
8+
client.POST("/posts", {
9+
body: {
10+
title: "My Post",
11+
body: "Post body",
12+
publish_date: new Date("2024-06-06T12:00:00Z").getTime(),
13+
// Regression test for #1769: extra body fields must still error when required fields are present.
14+
// @ts-expect-error
15+
undefined_property: true,
16+
},
17+
});
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test } from "vitest";
2+
import createClient from "../../src/index.js";
3+
import type { paths } from "./schemas/post.js";
4+
5+
test("client.request rejects undefined request body properties", () => {
6+
const client = createClient<paths>();
7+
8+
client.request("post", "/posts", {
9+
body: {
10+
title: "My Post",
11+
body: "Post body",
12+
publish_date: new Date("2024-06-06T12:00:00Z").getTime(),
13+
// Regression test for #1769 across client.request().
14+
// @ts-expect-error
15+
undefined_property: true,
16+
},
17+
});
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { test } from "vitest";
2+
import { createPathBasedClient } from "../../src/index.js";
3+
import type { paths } from "../http-methods/schemas/post.js";
4+
5+
test("path-based client rejects undefined request body properties", () => {
6+
const client = createPathBasedClient<paths>();
7+
8+
client["/posts"].POST({
9+
body: {
10+
title: "My Post",
11+
body: "Post body",
12+
publish_date: new Date("2024-06-06T12:00:00Z").getTime(),
13+
// Regression test for #1769 across path-based clients.
14+
// @ts-expect-error
15+
undefined_property: true,
16+
},
17+
});
18+
});

packages/openapi-react-query/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export default function createClient<Paths extends {}, Media extends MediaType =
207207
return data ?? null;
208208
}
209209

210-
return data;
210+
return data as any;
211211
};
212212

213213
const queryOptions: QueryOptionsFunction<Paths, Media> = (method, path, ...[init, options]) => ({
@@ -251,7 +251,7 @@ export default function createClient<Paths extends {}, Media extends MediaType =
251251
if (error) {
252252
throw error;
253253
}
254-
return data;
254+
return data as any;
255255
},
256256
...restOptions,
257257
},

0 commit comments

Comments
 (0)