Skip to content

Commit a8f2253

Browse files
authored
Fix fetching (#1719)
1 parent 7c7d715 commit a8f2253

23 files changed

+625
-150
lines changed

.changeset/fresh-icons-draw.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Remove nanoid from dependencies

.changeset/little-snails-shake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Fix "failed to execute fetch on Window" error

.changeset/twenty-nails-change.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-fetch": patch
3+
---
4+
5+
Revert customFetch API back to `fetch(input: Request)`

.github/workflows/ci.yml

+13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ jobs:
3939
run_install: true
4040
- run: pnpm run build
4141
- run: pnpm test
42+
test-e2e:
43+
runs-on: ubuntu-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
- uses: actions/setup-node@v4
47+
with:
48+
node-version: 22
49+
- uses: pnpm/action-setup@v4
50+
with:
51+
version: latest
52+
run_install: true
53+
- run: pnpm exec playwright install --with-deps
54+
- run: pnpm run test-e2e
4255
test-macos:
4356
runs-on: macos-latest
4457
steps:

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@
1212
"lint": "pnpm run -r --parallel --aggregate-output lint",
1313
"format": "pnpm run -r --parallel --aggregate-output format",
1414
"test": "pnpm run -r --parallel --aggregate-output test",
15+
"test-e2e": "pnpm run -r --parallel --aggregate-output test-e2e",
1516
"version": "pnpm run build && changeset version && pnpm i"
1617
},
1718
"devDependencies": {
1819
"@biomejs/biome": "^1.8.1",
1920
"@changesets/changelog-github": "^0.5.0",
2021
"@changesets/cli": "^2.27.5",
22+
"@playwright/test": "^1.44.1",
23+
"@types/node": "^20.14.7",
2124
"del-cli": "^5.1.0",
2225
"prettier": "^3.3.2",
23-
"typescript": "^5.4.5"
26+
"typescript": "^5.4.5",
27+
"vitest": "^1.6.0"
2428
}
2529
}

packages/openapi-fetch/examples/nextjs/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"react-dom": "18.3.1"
1313
},
1414
"devDependencies": {
15-
"@types/node": "20.14.6",
1615
"@types/react": "18.3.1",
1716
"@types/react-dom": "18.3.0",
1817
"openapi-typescript": "workspace:^",

packages/openapi-fetch/examples/vue-3/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
},
1717
"devDependencies": {
1818
"@tsconfig/node20": "^20.1.4",
19-
"@types/node": "^20.14.6",
2019
"@vitejs/plugin-vue": "^5.0.5",
2120
"@vue/tsconfig": "^0.5.1",
2221
"openapi-typescript": "workspace:^",

packages/openapi-fetch/package.json

+5-3
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,17 @@
5454
"build:cjs": "esbuild --bundle src/index.js --format=cjs --outfile=dist/cjs/index.cjs && cp dist/index.d.ts dist/cjs/index.d.cts",
5555
"format": "biome format . --write",
5656
"lint": "biome check .",
57-
"generate-types": "openapi-typescript test/fixtures/api.yaml -o test/fixtures/api.d.ts",
57+
"generate-types": "openapi-typescript -c test/redocly.yaml",
5858
"pretest": "pnpm run generate-types",
5959
"test": "pnpm run \"/^test:/\"",
6060
"test:js": "vitest run",
6161
"test:ts": "tsc --noEmit",
62+
"test-e2e": "playwright test",
63+
"e2e-vite-build": "vite build test/fixtures/e2e",
64+
"e2e-vite-start": "vite preview test/fixtures/e2e",
6265
"version": "pnpm run prepare && pnpm run build"
6366
},
6467
"dependencies": {
65-
"nanoid": "^5.0.7",
6668
"openapi-typescript-helpers": "workspace:^"
6769
},
6870
"devDependencies": {
@@ -76,6 +78,6 @@
7678
"openapi-typescript-fetch": "^2.0.0",
7779
"superagent": "^9.0.2",
7880
"typescript": "^5.4.5",
79-
"vitest": "^1.6.0"
81+
"vite": "^5.3.1"
8082
}
8183
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
const PORT = Number.parseInt(process.env.PORT || 4173 || "", 10);
4+
5+
export default defineConfig({
6+
testMatch: "test/**/*.e2e.ts",
7+
webServer: {
8+
command: "pnpm run e2e-vite-build && pnpm run e2e-vite-start",
9+
port: PORT,
10+
},
11+
use: {
12+
baseURL: `http://localhost:${PORT}`,
13+
},
14+
projects: [
15+
{
16+
name: "chrome",
17+
use: { ...devices["Desktop Chrome"] },
18+
},
19+
{
20+
name: "firefox",
21+
use: { ...devices["Desktop Firefox"] },
22+
},
23+
{
24+
name: "webkit",
25+
use: { ...devices["Desktop Safari"] },
26+
},
27+
],
28+
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface ClientOptions extends Omit<RequestInit, "headers"> {
1515
/** set the common root URL for all API requests */
1616
baseUrl?: string;
1717
/** custom fetch (defaults to globalThis.fetch) */
18-
fetch?: (input: string, init?: RequestInit) => Promise<Response>;
18+
fetch?: (input: Request) => Promise<Response>;
1919
/** global querySerializer */
2020
querySerializer?: QuerySerializer<unknown> | QuerySerializerOptions;
2121
/** global bodySerializer */

packages/openapi-fetch/src/index.js

+53-40
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { nanoid } from "nanoid";
2-
31
// settings & const
42
const DEFAULT_HEADERS = {
53
"Content-Type": "application/json",
@@ -21,6 +19,14 @@ class CustomRequest extends Request {
2119
}
2220
}
2321

22+
/**
23+
* Returns a cheap, non-cryptographically-secure random ID
24+
* Courtesy of @imranbarbhuiya (https://github.com/imranbarbhuiya)
25+
*/
26+
export function randomID() {
27+
return Math.random().toString(36).slice(2, 11);
28+
}
29+
2430
/**
2531
* Create an openapi-fetch client.
2632
* @type {import("./index.js").default}
@@ -84,56 +90,63 @@ export default function createClient(clientOptions) {
8490
requestInit.headers.delete("Content-Type");
8591
}
8692

87-
const id = nanoid();
93+
let id;
94+
let options;
8895
let request = new CustomRequest(createFinalURL(schemaPath, { baseUrl, params, querySerializer }), requestInit);
8996

90-
// middleware (request)
91-
const options = Object.freeze({
92-
baseUrl,
93-
fetch,
94-
parseAs,
95-
querySerializer,
96-
bodySerializer,
97-
});
98-
for (const m of middlewares) {
99-
if (m && typeof m === "object" && typeof m.onRequest === "function") {
100-
const result = await m.onRequest({
101-
request,
102-
schemaPath,
103-
params,
104-
options,
105-
id,
106-
});
107-
if (result) {
108-
if (!(result instanceof Request)) {
109-
throw new Error("onRequest: must return new Request() when modifying the request");
97+
if (middlewares.length) {
98+
id = randomID();
99+
100+
// middleware (request)
101+
options = Object.freeze({
102+
baseUrl,
103+
fetch,
104+
parseAs,
105+
querySerializer,
106+
bodySerializer,
107+
});
108+
for (const m of middlewares) {
109+
if (m && typeof m === "object" && typeof m.onRequest === "function") {
110+
const result = await m.onRequest({
111+
request,
112+
schemaPath,
113+
params,
114+
options,
115+
id,
116+
});
117+
if (result) {
118+
if (!(result instanceof Request)) {
119+
throw new Error("onRequest: must return new Request() when modifying the request");
120+
}
121+
request = result;
110122
}
111-
request = result;
112123
}
113124
}
114125
}
115126

116127
// fetch!
117-
let response = await fetch(request.url, request);
128+
let response = await fetch(request);
118129

119130
// middleware (response)
120131
// execute in reverse-array order (first priority gets last transform)
121-
for (let i = middlewares.length - 1; i >= 0; i--) {
122-
const m = middlewares[i];
123-
if (m && typeof m === "object" && typeof m.onResponse === "function") {
124-
const result = await m.onResponse({
125-
request,
126-
response,
127-
schemaPath,
128-
params,
129-
options,
130-
id,
131-
});
132-
if (result) {
133-
if (!(result instanceof Response)) {
134-
throw new Error("onResponse: must return new Response() when modifying the response");
132+
if (middlewares.length) {
133+
for (let i = middlewares.length - 1; i >= 0; i--) {
134+
const m = middlewares[i];
135+
if (m && typeof m === "object" && typeof m.onResponse === "function") {
136+
const result = await m.onResponse({
137+
request,
138+
response,
139+
schemaPath,
140+
params,
141+
options,
142+
id,
143+
});
144+
if (result) {
145+
if (!(result instanceof Response)) {
146+
throw new Error("onResponse: must return new Response() when modifying the response");
147+
}
148+
response = result;
135149
}
136-
response = result;
137150
}
138151
}
139152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": "passed",
3+
"failedTests": []
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import createClient from "../../../src";
2+
import type { paths } from "./e2e.d.ts";
3+
4+
const client = createClient<paths>({
5+
baseUrl: "/api/v1",
6+
});
7+
8+
/**
9+
* Test 1: GET /api/v1/get
10+
*/
11+
async function testGet() {
12+
const { data } = await client.GET("/get");
13+
if (!data) {
14+
throw new Error("/get: No data");
15+
}
16+
}
17+
18+
/**
19+
* Test 2: POST /api/v1/post
20+
*/
21+
async function testPost() {
22+
const { data } = await client.POST("/post", { body: { message: "POST" } });
23+
if (!data) {
24+
throw new Error("/post: No data");
25+
}
26+
}
27+
28+
/**
29+
* Test 3: PUT /api/v1/multi-form
30+
*/
31+
async function testMultiForm() {
32+
const { data } = await client.POST("/multi-form", {
33+
body: {
34+
message: "Form",
35+
file: new File(["Hello, World!"], "hello.txt") as unknown as string,
36+
},
37+
});
38+
if (!data) {
39+
throw new Error("/multi-form: No data");
40+
}
41+
}
42+
43+
// run all tests immediately on load
44+
(async () => {
45+
await Promise.all([testGet(), testPost(), testMultiForm()]);
46+
47+
// add element Playwright is waiting for
48+
const div = document.createElement("div");
49+
div.setAttribute("data-status", "success");
50+
div.innerHTML = "Success";
51+
document.body.appendChild(div);
52+
})();

0 commit comments

Comments
 (0)