Skip to content

Add client option to pass custom RequestInit object into fetch requests for supported implementations #2020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 2, 2024
Merged
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/angry-emus-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-fetch": patch
---

Add client option to pass custom RequestInit object into fetch requests for supported implementations
3 changes: 3 additions & 0 deletions packages/openapi-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@
"del-cli": "^5.1.0",
"esbuild": "^0.24.0",
"execa": "^8.0.1",
"express": "^4.21.1",
"feature-fetch": "^0.0.15",
"node-forge": "^1.3.1",
"openapi-typescript": "workspace:^",
"openapi-typescript-codegen": "^0.25.0",
"openapi-typescript-fetch": "^2.0.0",
"superagent": "^10.1.1",
"typescript": "^5.7.2",
"undici": "^6.21.0",
"vite": "^6.0.1"
}
}
2 changes: 2 additions & 0 deletions packages/openapi-fetch/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface ClientOptions extends Omit<RequestInit, "headers"> {
/** global bodySerializer */
bodySerializer?: BodySerializer<unknown>;
headers?: HeadersOptions;
/** RequestInit extension object to pass as 2nd argument to fetch when supported (defaults to undefined) */
requestInitExt?: Record<string, unknown>;
}

export type HeadersOptions =
Expand Down
12 changes: 11 additions & 1 deletion packages/openapi-fetch/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// settings & const
const PATH_PARAM_RE = /\{[^{}]+\}/g;

const supportsRequestInitExt = () => {
return (
typeof process === "object" &&
Number.parseInt(process?.versions?.node?.substring(0, 2)) >= 18 &&
process.versions.undici
);
};

/**
* Returns a cheap, non-cryptographically-secure random ID
* Courtesy of @imranbarbhuiya (https://github.com/imranbarbhuiya)
Expand All @@ -21,8 +29,10 @@ export default function createClient(clientOptions) {
querySerializer: globalQuerySerializer,
bodySerializer: globalBodySerializer,
headers: baseHeaders,
requestInitExt = undefined,
...baseOptions
} = { ...clientOptions };
requestInitExt = supportsRequestInitExt() ? requestInitExt : undefined;
baseUrl = removeTrailingSlash(baseUrl);
const middlewares = [];

Expand Down Expand Up @@ -126,7 +136,7 @@ export default function createClient(clientOptions) {
// fetch!
let response;
try {
response = await fetch(request);
response = await fetch(request, requestInitExt);
} catch (error) {
let errorAfterMiddleware = error;
// middleware (error)
Expand Down
92 changes: 92 additions & 0 deletions packages/openapi-fetch/test/common/create-client-e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import express from "express";
import { expect, test } from "vitest";
import * as https from "node:https";
import { Agent } from "undici";
import createClient from "../../src/index.js";
import * as forge from "node-forge";
import * as crypto from "node:crypto";

const pki = forge.pki;

const genCACert = async (opts = {}) => {
const options = {
...{
commonName: "Testing CA - DO NOT TRUST",
bits: 2048,
},
...opts,
};

const keyPair = await new Promise((res, rej) => {
pki.rsa.generateKeyPair({ bits: options.bits }, (error, pair) => {
if (error) {
rej(error);
} else {
res(pair);
}
});
});

const cert = pki.createCertificate();
cert.publicKey = keyPair.publicKey;
cert.serialNumber = crypto.randomUUID().replace(/-/g, "");

cert.validity.notBefore = new Date();
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);

cert.setSubject([{ name: "commonName", value: options.commonName }]);
cert.setExtensions([{ name: "basicConstraints", cA: true }]);

cert.setIssuer(cert.subject.attributes);
cert.sign(keyPair.privateKey, forge.md.sha256.create());

return {
ca: {
key: pki.privateKeyToPem(keyPair.privateKey),
cert: pki.certificateToPem(cert),
},
fingerprint: forge.util.encode64(
pki.getPublicKeyFingerprint(keyPair.publicKey, {
type: "SubjectPublicKeyInfo",
md: forge.md.sha256.create(),
encoding: "binary",
}),
),
};
};

const caToBuffer = (ca) => {
return {
key: Buffer.from(ca.key),
cert: Buffer.from(ca.cert),
};
};

const API_PORT = process.env.API_PORT || 4578;

const app = express();
app.get("/v1/foo", (req, res) => {
res.send("bar");
});

test("requestInitExt", async () => {
const cert = await genCACert();
const buffers = caToBuffer(cert.ca);
const options = {};
options.key = buffers.key;
options.cert = buffers.cert;
const httpsServer = https.createServer(options, app);
httpsServer.listen(4578);
const dispatcher = new Agent({
connect: {
rejectUnauthorized: false,
},
});
const client = createClient({ baseUrl: `https://localhost:${API_PORT}`, requestInitExt: { dispatcher } });
const fetchResponse = await client.GET("/v1/foo", { parseAs: "text" });
httpsServer.closeAllConnections();
httpsServer.close();
expect(fetchResponse.response.ok).toBe(true);
});
Loading
Loading