refactor: patch api client to have a global client per call

This commit is contained in:
Nicolas Meienberger 2026-02-11 19:31:42 +01:00
parent 9422815843
commit 0da4b45d67
6 changed files with 136 additions and 24 deletions

View file

@ -1,19 +1,27 @@
// @ts-nocheck // @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from "./client"; import { type ClientOptions, type Config, createClient, createConfig } from "./client";
import type { ClientOptions as ClientOptions2 } from "./types.gen"; import type { ClientOptions as ClientOptions2 } from "./types.gen";
import { getRequestClient } from "../../lib/request-client";
/**
* The `createClientConfig()` function will be called on client initialization
* and the returned object will become the client's initial configuration.
*
* You may want to initialize your client this way instead of calling
* `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values.
*/
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = ( export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
override?: Config<ClientOptions & T>, override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>; ) => Config<Required<ClientOptions> & T>;
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" })); const fallbackClient = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
/**
* Proxy client that automatically uses the per-request client from request-scoped server storage.
* Falls back to a default client if no request context is available.
*/
export const client = new Proxy(fallbackClient, {
get(target, prop, receiver) {
try {
const requestClient = getRequestClient();
return Reflect.get(requestClient, prop, receiver);
} catch {
// No request context available, use fallback
return Reflect.get(target, prop, receiver);
}
},
});

57
app/lib/request-client.ts Normal file
View file

@ -0,0 +1,57 @@
import type { Config } from "~/client/api-client/client";
import { createClient, createConfig } from "~/client/api-client/client";
export type RequestClient = ReturnType<typeof createClient>;
type RequestClientStore = {
getStore: () => RequestClient | undefined;
run: <T>(client: RequestClient, fn: () => T) => T;
};
type AsyncLocalStorageConstructor = new <T>() => {
getStore: () => T | undefined;
run: <R>(store: T, callback: () => R) => R;
};
let requestClientStore: RequestClientStore | undefined;
const ASYNC_HOOKS_MODULE = "node:async_hooks";
const loadRequestClientStore = async (): Promise<RequestClientStore | undefined> => {
if (typeof window !== "undefined") {
return undefined;
}
if (!requestClientStore) {
const asyncHooksModule = (await import(/* @vite-ignore */ ASYNC_HOOKS_MODULE)) as {
AsyncLocalStorage: AsyncLocalStorageConstructor;
};
requestClientStore = new asyncHooksModule.AsyncLocalStorage<RequestClient>();
}
return requestClientStore;
};
export function getRequestClient(): RequestClient {
const client = requestClientStore?.getStore();
if (!client) {
throw new Error("No request client available");
}
return client;
}
export async function runWithRequestClient<T>(client: RequestClient, fn: () => T): Promise<T> {
const store = await loadRequestClientStore();
if (!store) {
return fn();
}
return store.run(client, fn);
}
export function createRequestClient(config: Config): RequestClient {
return createClient(createConfig(config));
}

View file

@ -1,14 +1,17 @@
import { createMiddleware } from "@tanstack/react-start"; import { createMiddleware } from "@tanstack/react-start";
import { client } from "~/client/api-client/client.gen";
import { getRequestHeaders } from "@tanstack/react-start/server"; import { getRequestHeaders } from "@tanstack/react-start/server";
import {
createRequestClient,
runWithRequestClient,
} from "~/lib/request-client";
export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => { export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => {
client.setConfig({ const client = createRequestClient({
baseUrl: `${new URL(request.url).origin}`, baseUrl: new URL(request.url).origin,
headers: { headers: {
cookie: getRequestHeaders().get("cookie") ?? "", cookie: getRequestHeaders().get("cookie") ?? "",
}, },
}); });
return next(); return runWithRequestClient(client, () => next());
}); });

View file

@ -9,7 +9,10 @@ export default defineConfig({
"// @ts-nocheck", "// @ts-nocheck",
"// This file is auto-generated by @hey-api/openapi-ts", "// This file is auto-generated by @hey-api/openapi-ts",
], ],
postProcess: ["oxfmt"], postProcess: [
"oxfmt",
{ command: "bun", args: ["scripts/patch-api-client.ts"] },
],
}, },
plugins: [...defaultPlugins, "@tanstack/react-query", "@hey-api/client-fetch"], plugins: [...defaultPlugins, "@tanstack/react-query", "@hey-api/client-fetch"],
}); });

View file

@ -1,9 +0,0 @@
import type { Config } from "@react-router/dev/config";
export default {
buildDirectory: "dist",
ssr: true,
future: {
v8_middleware: true,
},
} satisfies Config;

50
scripts/patch-api-client.ts Executable file
View file

@ -0,0 +1,50 @@
#!/usr/bin/env node
/**
* Post-process script for openapi-ts generation.
* Patches client.gen.ts to use request-scoped clients from server storage,
* preventing cross-request cookie/origin leakage during SSR.
*/
/* eslint-disable no-console */
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
const CLIENT_GEN_PATH = resolve(process.cwd(), "app/client/api-client/client.gen.ts");
const PATCHED_CONTENT = `// @ts-nocheck
// This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
import type { ClientOptions as ClientOptions2 } from "./types.gen";
import { getRequestClient } from "../../lib/request-client";
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
const fallbackClient = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
/**
* Proxy client that automatically uses the per-request client from request-scoped server storage.
* Falls back to a default client if no request context is available.
*/
export const client = new Proxy(fallbackClient, {
get(target, prop, receiver) {
try {
const requestClient = getRequestClient();
return Reflect.get(requestClient, prop, receiver);
} catch {
// No request context available, use fallback
return Reflect.get(target, prop, receiver);
}
},
});
`;
try {
writeFileSync(CLIENT_GEN_PATH, PATCHED_CONTENT);
console.log("✓ Patched client.gen.ts with request-scoped client support");
} catch (error) {
console.error("✗ Failed to patch client.gen.ts:", error instanceof Error ? error.message : String(error));
process.exit(1);
}