Skip to content

Commit 7081842

Browse files
authored
Add client option to pass custom RequestInit object into fetch requests for supported implementations (#2020)
* Add client option to pass custom RequestInit object into fetch requests for supported implementations * Add client option to pass custom RequestInit object into fetch requests for supported implementations - formatting * Add client option to pass custom RequestInit object into fetch requests for supported implementations - formatting * Add client option to pass custom RequestInit object into fetch requests for supported implementations - fix e2e test failures * Add client option to pass custom RequestInit object into fetch requests for supported implementations - refactor test to use own server/endpoint * Add client option to pass custom RequestInit object into fetch requests for supported implementations - refactor test to use own server/endpoint * Add client option to pass custom RequestInit object into fetch requests for supported implementations - refactor test to use own server/endpoint * Add client option to pass custom RequestInit object into fetch requests for supported implementations - refactor test to use own server/endpoint * Add client option to pass custom RequestInit object into fetch requests for supported implementations - fix lockfile * Add client option to pass custom RequestInit object into fetch requests for supported implementations - add changeset * Add client option to pass custom RequestInit object into fetch requests for supported implementations - add changeset
1 parent 75399f8 commit 7081842

File tree

6 files changed

+498
-1
lines changed

6 files changed

+498
-1
lines changed

.changeset/angry-emus-hunt.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+
Add client option to pass custom RequestInit object into fetch requests for supported implementations

packages/openapi-fetch/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,15 @@
7474
"del-cli": "^5.1.0",
7575
"esbuild": "^0.24.0",
7676
"execa": "^8.0.1",
77+
"express": "^4.21.1",
7778
"feature-fetch": "^0.0.15",
79+
"node-forge": "^1.3.1",
7880
"openapi-typescript": "workspace:^",
7981
"openapi-typescript-codegen": "^0.25.0",
8082
"openapi-typescript-fetch": "^2.0.0",
8183
"superagent": "^10.1.1",
8284
"typescript": "^5.7.2",
85+
"undici": "^6.21.0",
8386
"vite": "^6.0.1"
8487
}
8588
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export interface ClientOptions extends Omit<RequestInit, "headers"> {
2424
/** global bodySerializer */
2525
bodySerializer?: BodySerializer<unknown>;
2626
headers?: HeadersOptions;
27+
/** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
28+
requestInitExt?: Record<string, unknown>;
2729
}
2830

2931
export type HeadersOptions =

packages/openapi-fetch/src/index.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
// settings & const
22
const PATH_PARAM_RE = /\{[^{}]+\}/g;
33

4+
const supportsRequestInitExt = () => {
5+
return (
6+
typeof process === "object" &&
7+
Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 &&
8+
process.versions.undici
9+
);
10+
};
11+
412
/**
513
* Returns a cheap, non-cryptographically-secure random ID
614
* Courtesy of @imranbarbhuiya (https://github.com/imranbarbhuiya)
@@ -21,8 +29,10 @@ export default function createClient(clientOptions) {
2129
querySerializer: globalQuerySerializer,
2230
bodySerializer: globalBodySerializer,
2331
headers: baseHeaders,
32+
requestInitExt = undefined,
2433
...baseOptions
2534
} = { ...clientOptions };
35+
requestInitExt = supportsRequestInitExt() ? requestInitExt : undefined;
2636
baseUrl = removeTrailingSlash(baseUrl);
2737
const middlewares = [];
2838

@@ -126,7 +136,7 @@ export default function createClient(clientOptions) {
126136
// fetch!
127137
let response;
128138
try {
129-
response = await fetch(request);
139+
response = await fetch(request, requestInitExt);
130140
} catch (error) {
131141
let errorAfterMiddleware = error;
132142
// middleware (error)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import express from "express";
2+
import { expect, test } from "vitest";
3+
import * as https from "node:https";
4+
import { Agent } from "undici";
5+
import createClient from "../../src/index.js";
6+
import * as forge from "node-forge";
7+
import * as crypto from "node:crypto";
8+
9+
const pki = forge.pki;
10+
11+
const genCACert = async (opts = {}) => {
12+
const options = {
13+
...{
14+
commonName: "Testing CA - DO NOT TRUST",
15+
bits: 2048,
16+
},
17+
...opts,
18+
};
19+
20+
const keyPair = await new Promise((res, rej) => {
21+
pki.rsa.generateKeyPair({ bits: options.bits }, (error, pair) => {
22+
if (error) {
23+
rej(error);
24+
} else {
25+
res(pair);
26+
}
27+
});
28+
});
29+
30+
const cert = pki.createCertificate();
31+
cert.publicKey = keyPair.publicKey;
32+
cert.serialNumber = crypto.randomUUID().replace(/-/g, "");
33+
34+
cert.validity.notBefore = new Date();
35+
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
36+
cert.validity.notAfter = new Date();
37+
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
38+
39+
cert.setSubject([{ name: "commonName", value: options.commonName }]);
40+
cert.setExtensions([{ name: "basicConstraints", cA: true }]);
41+
42+
cert.setIssuer(cert.subject.attributes);
43+
cert.sign(keyPair.privateKey, forge.md.sha256.create());
44+
45+
return {
46+
ca: {
47+
key: pki.privateKeyToPem(keyPair.privateKey),
48+
cert: pki.certificateToPem(cert),
49+
},
50+
fingerprint: forge.util.encode64(
51+
pki.getPublicKeyFingerprint(keyPair.publicKey, {
52+
type: "SubjectPublicKeyInfo",
53+
md: forge.md.sha256.create(),
54+
encoding: "binary",
55+
}),
56+
),
57+
};
58+
};
59+
60+
const caToBuffer = (ca) => {
61+
return {
62+
key: Buffer.from(ca.key),
63+
cert: Buffer.from(ca.cert),
64+
};
65+
};
66+
67+
const API_PORT = process.env.API_PORT || 4578;
68+
69+
const app = express();
70+
app.get("/v1/foo", (req, res) => {
71+
res.send("bar");
72+
});
73+
74+
test("requestInitExt", async () => {
75+
const cert = await genCACert();
76+
const buffers = caToBuffer(cert.ca);
77+
const options = {};
78+
options.key = buffers.key;
79+
options.cert = buffers.cert;
80+
const httpsServer = https.createServer(options, app);
81+
httpsServer.listen(4578);
82+
const dispatcher = new Agent({
83+
connect: {
84+
rejectUnauthorized: false,
85+
},
86+
});
87+
const client = createClient({ baseUrl: `https://localhost:${API_PORT}`, requestInitExt: { dispatcher } });
88+
const fetchResponse = await client.GET("/v1/foo", { parseAs: "text" });
89+
httpsServer.closeAllConnections();
90+
httpsServer.close();
91+
expect(fetchResponse.response.ok).toBe(true);
92+
});

0 commit comments

Comments
 (0)